Commit fd92307c40 for ffmpeg
commit fd92307c406bcbdf0d3eac25f223e80fc479c833
Author: Niklas Haas <git@haasn.dev>
Date: Sun Oct 4 18:34:38 2026 +0200
avformat/jpegmpfdec: add JPEG Multi-Picture Format demuxer
This format is only auto-probed when we can be certain that the image is a
JPEG MPF file; as evidenced either by the extension (.mpo), or by the
presence of an MPF APP2 marker or an ISO21496 gain map stub.
This does require parsing possibly arbitrarily far into the file, since
the MPF header can be after a > 2 KB EXIF header; and this matches files
produced in the wild (by e.g. Apple's jpeg ultra hdr encoder).
The probe function returns AVERROR(EAGAIN) to request more data until it
reaches either the MPF marker or the start of the entropy coded data. If
the probe limit or EOF is hit first, the best scoring other format wins,
e.g. image2 for regular .jpg files.
A few additional notes:
- libultrahdr produces quite spec-incompliant files which we unfortunately
have to work around
- since the MPF format table can be arbitrarily far from the header, we
also need to store possibly a lot of data and re-return it to the caller;
I have followed the convention established by e.g. fitsdec.c of just keeping
a spare AVPacket for bytes already read during header parsing (cf. also
jpegxl_anim_dec.c)
Signed-off-by: Niklas Haas <git@haasn.dev>
diff --git a/Changelog b/Changelog
index 518a469de1..055b0cfa79 100644
--- a/Changelog
+++ b/Changelog
@@ -2,6 +2,7 @@ Entries are sorted chronologically from oldest to youngest within each release,
releases are sorted from youngest to oldest.
version <next>:
+- JPEG Multi-Picture Format demuxer
version 9.1:
- ASTC image encoding and decoding via libastcenc, with .astc and KTX 1.0 muxers and demuxers
diff --git a/configure b/configure
index 3bb4d9ef80..083bd652c4 100755
--- a/configure
+++ b/configure
@@ -3928,6 +3928,7 @@ imf_demuxer_select="mxf_demuxer"
ipod_muxer_select="mov_muxer"
ismv_muxer_select="mov_muxer"
ivf_muxer_select="av1_metadata_bsf vp9_superframe_bsf"
+jpeg_mpf_demuxer_select="mjpeg_parser"
latm_muxer_select="aac_adtstoasc_bsf mpeg4audio"
matroska_audio_muxer_select="matroska_muxer"
matroska_demuxer_select="riffdec"
diff --git a/doc/demuxers.texi b/doc/demuxers.texi
index 7610055193..5b56e8b476 100644
--- a/doc/demuxers.texi
+++ b/doc/demuxers.texi
@@ -746,6 +746,20 @@ ffmpeg -framerate 10 -pattern_type glob -i "*.png" out.mkv
@end example
@end itemize
+@section jpeg_mpf
+
+JPEG Multi-Picture Format demuxer.
+
+Exposes each JPEG image stored in a CIPA DC-007 MP file as a separate stream,
+in the order of the MP Entries, with the First Individual Image flagged as the
+default stream. If the file contains an ISO 21496-1 gain map, as is the case
+for JPEG Ultra HDR, the primary image and the gain map are linked by a
+@samp{gain_map} stream group, and the gain map parameters are exported as
+frame side data by the decoder.
+
+Images of any other type (e.g. large thumbnails or multi-view images) are
+exposed as additional streams.
+
@section ktx
Khronos Texture 1.0 demuxer for ASTC textures.
diff --git a/doc/general_contents.texi b/doc/general_contents.texi
index 3e0c24e4d1..cfaf4252c6 100644
--- a/doc/general_contents.texi
+++ b/doc/general_contents.texi
@@ -650,6 +650,8 @@ library:
@tab A format used by libvpx
@item Internet Video Recording @tab @tab X
@item IRCAM @tab X @tab X
+@item JPEG Multi-Picture Format @tab X @tab X
+ @tab Includes JPEG Ultra HDR (ISO 21496-1 gain map) images.
@item LAF @tab @tab X
@tab Limitless Audio Format
@item LATM @tab X @tab X
diff --git a/libavformat/Makefile b/libavformat/Makefile
index 3c875d2da6..dbf78aa4b9 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -346,6 +346,7 @@ OBJS-$(CONFIG_IVF_MUXER) += ivfenc.o
OBJS-$(CONFIG_IVR_DEMUXER) += rmdec.o rm.o rmsipr.o
OBJS-$(CONFIG_JACOSUB_DEMUXER) += jacosubdec.o subtitles.o
OBJS-$(CONFIG_JACOSUB_MUXER) += jacosubenc.o rawenc.o
+OBJS-$(CONFIG_JPEG_MPF_DEMUXER) += jpegmpfdec.o
OBJS-$(CONFIG_JPEG_MPF_MUXER) += jpegmpfenc.o
OBJS-$(CONFIG_JPEGXL_ANIM_DEMUXER) += jpegxl_anim_dec.o
OBJS-$(CONFIG_JV_DEMUXER) += jvdec.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index c0da275626..a4e859da33 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -251,6 +251,7 @@ extern const FFInputFormat ff_ivr_demuxer;
extern const FFInputFormat ff_jacosub_demuxer;
extern const FFOutputFormat ff_jacosub_muxer;
extern const FFInputFormat ff_jv_demuxer;
+extern const FFInputFormat ff_jpeg_mpf_demuxer;
extern const FFOutputFormat ff_jpeg_mpf_muxer;
extern const FFInputFormat ff_jpegxl_anim_demuxer;
extern const FFInputFormat ff_ktx_demuxer;
diff --git a/libavformat/jpegmpf.h b/libavformat/jpegmpf.h
index 69a6ab5a18..0cf1a6522e 100644
--- a/libavformat/jpegmpf.h
+++ b/libavformat/jpegmpf.h
@@ -30,16 +30,37 @@
enum MPFAttribute {
MPF_DEPENDENT_PARENT = 0x80000000,
MPF_DEPENDENT_CHILD = 0x40000000,
+ MPF_REPRESENTATIVE = 0x20000000,
+ MPF_ATTR_FORMAT_MASK = 0x07000000,
+ MPF_ATTR_TYPE_MASK = 0x00FFFFFF,
MPF_FORMAT_JPEG = 0x00000000,
};
/* MP Type Codes, from CIPA DC-007 Table 4 */
enum MPFType {
- MPF_TYPE_UNDEFINED = 0x000000,
- MPF_TYPE_PRIMARY = 0x030000, /* Baseline MP Primary Image */
- MPF_TYPE_GAIN_MAP = 0x050000,
+ MPF_TYPE_UNDEFINED = 0x000000,
+ MPF_TYPE_LARGE_THUMBNAIL = 0x010000, /* + class (1-5) */
+ MPF_TYPE_MULTI_FRAME = 0x020000, /* + panorama / disparity / multi-angle */
+ MPF_TYPE_PRIMARY = 0x030000, /* Baseline MP Primary Image */
+ MPF_TYPE_ORIGINAL = 0x040000, /* Original preservation image */
+ MPF_TYPE_GAIN_MAP = 0x050000,
+
+ MPF_TYPE_INFO_MASK = 0xFF0000, /* see CIPA DC-007 Figure 9 */
};
+/* Returns whether images of this type only exist as dependent images */
+static inline int mpf_type_is_child(enum MPFType type)
+{
+ switch (type & MPF_TYPE_INFO_MASK) {
+ case MPF_TYPE_LARGE_THUMBNAIL:
+ case MPF_TYPE_ORIGINAL:
+ case MPF_TYPE_GAIN_MAP:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
/* MP Index IFD tags, from CIPA DC-007 Table 3 */
enum MPFTag {
MPF_TAG_MPFVersion = 0xB000,
diff --git a/libavformat/jpegmpfdec.c b/libavformat/jpegmpfdec.c
new file mode 100644
index 0000000000..8dad0de780
--- /dev/null
+++ b/libavformat/jpegmpfdec.c
@@ -0,0 +1,507 @@
+/*
+ * JPEG Multi-Picture Format demuxer
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Demuxer for the JPEG Multi-Picture Format file format.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "libavutil/intreadwrite.h"
+#include "libavutil/gain_map.h"
+#include "libavutil/mem.h"
+
+#include "libavcodec/bytestream.h"
+#include "libavcodec/exif.h"
+#include "libavcodec/mjpeg.h"
+
+#include "avformat.h"
+#include "demux.h"
+#include "jpegmpf.h"
+#include "internal.h"
+
+typedef struct MPFDemuxImage {
+ AVStream *st;
+ int64_t start;
+ int64_t size;
+
+ uint32_t type;
+ int index; /* of the corresponding MP Entry */
+ int child; /* is a dependent image */
+} MPFDemuxImage;
+
+typedef struct MPFDemuxContext {
+ MPFDemuxImage *images; /* sorted by position in the file */
+ int nb_images;
+
+ int cur_image; /* index of next image to return */
+ int has_gain_map; /* the primary image carries ISO 21496-1 metadata */
+ int passthrough; /* not an MPF file */
+ int first_img; /* index of the first individual image */
+
+ /* image data parsed during mpf_read_header() */
+ AVPacket *initial;
+ int64_t file_start;
+} MPFDemuxContext;
+
+#define CHECK_IDENT(strc) (len >= sizeof(strc) && !memcmp(gb->buffer, strc, sizeof(strc)))
+
+static int mpf_probe(const AVProbeData *p)
+{
+ GetByteContext gbc, *gb = &gbc;
+ bytestream2_init(gb, p->buf, p->buf_size);
+ if (bytestream2_get_be16(gb) != (0xFF00 | SOI))
+ return 0;
+
+ while (bytestream2_get_bytes_left(gb) >= 4) {
+ if (bytestream2_get_byte(gb) != 0xFF)
+ return 0; /* not a JPEG marker */
+
+ int marker = bytestream2_get_byte(gb);
+ while (marker == 0xFF && bytestream2_get_bytes_left(gb) > 0) /* fill bytes */
+ marker = bytestream2_get_byte(gb);
+
+ if (marker == SOS || marker == TEM || (marker >= RST0 && marker <= EOI))
+ return 0; /* start of entropy coded data */
+ else if (marker == 0xFF || bytestream2_get_bytes_left(gb) < 2)
+ break; /* need more data */
+
+ const int len = bytestream2_get_be16(gb) - 2;
+ if (len < 0)
+ return 0;
+ if (len > bytestream2_get_bytes_left(gb))
+ break;
+
+ switch (marker) {
+ case APP2:
+ /* Marker segments unique to MPF files; the base image of a gain
+ * map file only carries the version fields of the ISO 21496-1
+ * metadata (two 16-bit integers); we don't want to false positive
+ * on standalone JPEGs that happen to contain gain map metadata */
+ if (CHECK_IDENT("MPF") ||
+ (CHECK_IDENT(AV_ISO21496_IDENTIFIER) &&
+ len == sizeof(AV_ISO21496_IDENTIFIER) + sizeof(uint16_t[2])))
+ return AVPROBE_SCORE_EXTENSION + 10;
+ break;
+ }
+
+ bytestream2_skip(gb, len);
+ }
+
+ /* If we ran out of probe bytes, we don't know yet whether this is an
+ * MPF file or not, unless the extension is unique to MPF files */
+ if (av_match_ext(p->filename, "mpo"))
+ return AVPROBE_SCORE_EXTENSION + 5;
+
+ return AVERROR(EAGAIN);
+}
+
+/* returns the offset of the first byte read */
+static int grow_header(AVFormatContext *ctx, int size)
+{
+ MPFDemuxContext *s = ctx->priv_data;
+ const int pos = s->initial->size;
+ int ret = av_append_packet(ctx->pb, s->initial, size);
+ if (ret < 0)
+ return ret;
+ if (ret < size)
+ return AVERROR_INVALIDDATA;
+ return pos;
+}
+
+static int parse_mp_index(AVFormatContext *ctx, const uint8_t *data, int len,
+ int64_t tiff_base)
+{
+ MPFDemuxContext *s = ctx->priv_data;
+ AVExifMetadata ifd = { 0 };
+ AVExifEntry *num = NULL, *entries = NULL;
+ int ret;
+
+ if (len < 2)
+ return AVERROR_INVALIDDATA;
+ const int le = AV_RB16(data) == AV_RB16("II");
+
+ ret = av_exif_parse_buffer(ctx, data, len, &ifd, AV_EXIF_TIFF_HEADER);
+ if (ret < 0)
+ return ret;
+
+ if ((ret = av_exif_get_entry(ctx, &ifd, MPF_TAG_NumberOfImages, 0, &num)) < 0 ||
+ (ret = av_exif_get_entry(ctx, &ifd, MPF_TAG_MPEntry, 0, &entries)) < 0)
+ goto fail;
+
+ if (!num || num->type != AV_TIFF_LONG || num->count != 1 ||
+ !entries || entries->type != AV_TIFF_UNDEFINED) {
+ av_log(ctx, AV_LOG_ERROR, "Missing or malformed MP Index IFD\n");
+ ret = AVERROR_INVALIDDATA;
+ goto fail;
+ }
+
+ /* The spec asserts MPEntry count = 16 * NumberOfImages, but tolerate
+ * excess entries, e.g. left behind after deleting an image */
+ const int entry_size = sizeof(uint32_t[3]) + sizeof(uint16_t[2]);
+ const uint64_t nb_images = num->value.uint[0];
+ if (!nb_images || nb_images * entry_size > entries->count) {
+ av_log(ctx, AV_LOG_ERROR, "Invalid number of images: %"PRIu64"\n",
+ nb_images);
+ ret = AVERROR_INVALIDDATA;
+ goto fail;
+ }
+
+ s->images = av_calloc(nb_images, sizeof(*s->images));
+ if (!s->images) {
+ ret = AVERROR(ENOMEM);
+ goto fail;
+ }
+
+ GetByteContext gb;
+ bytestream2_init(&gb, entries->value.ubytes, entries->count);
+
+ const int explode = ctx->error_recognition & AV_EF_EXPLODE;
+ for (int i = 0; i < nb_images; i++) {
+ const uint32_t attr = le ? bytestream2_get_le32u(&gb) : bytestream2_get_be32u(&gb);
+ const uint32_t size = le ? bytestream2_get_le32u(&gb) : bytestream2_get_be32u(&gb);
+ const uint32_t offset = le ? bytestream2_get_le32u(&gb) : bytestream2_get_be32u(&gb);
+ bytestream2_skipu(&gb, sizeof(uint16_t[2])); /* dependent images */
+
+ if ((attr & MPF_ATTR_FORMAT_MASK) != MPF_FORMAT_JPEG) {
+ const int fatal = !offset || explode;
+ av_log(ctx, fatal ? AV_LOG_ERROR : AV_LOG_WARNING, "Image %d: "
+ "unsupported format 0x%"PRIx32"\n", i,
+ attr & MPF_ATTR_FORMAT_MASK);
+ if (fatal) {
+ ret = AVERROR_INVALIDDATA;
+ goto fail;
+ }
+ continue;
+ }
+
+ /* the First Individual Image is identified by an offset of 0 */
+ if (!offset) {
+ if (s->first_img >= 0) {
+ av_log(ctx, AV_LOG_ERROR, "Duplicate First Individual Image\n");
+ ret = AVERROR_INVALIDDATA;
+ goto fail;
+ }
+ s->first_img = s->nb_images;
+ }
+
+ MPFDemuxImage *img = &s->images[s->nb_images++];
+ img->index = i;
+ img->start = offset ? tiff_base + offset : s->file_start;
+ img->size = size;
+ img->type = attr & MPF_ATTR_TYPE_MASK;
+ img->child = !!(attr & MPF_DEPENDENT_CHILD);
+ }
+
+ ret = 0;
+fail:
+ av_exif_free(&ifd);
+ return ret;
+}
+
+static int parse_app2(AVFormatContext *ctx, int offset, int len)
+{
+ MPFDemuxContext *s = ctx->priv_data;
+ GetByteContext gbc, *gb = &gbc;
+ bytestream2_init(gb, s->initial->data + offset, len);
+
+ if (CHECK_IDENT(AV_ISO21496_IDENTIFIER)) {
+ s->has_gain_map = 1;
+ } else if (CHECK_IDENT("MPF")) {
+ if (s->images) {
+ const int explode = ctx->error_recognition & AV_EF_EXPLODE;
+ av_log(ctx, explode ? AV_LOG_ERROR : AV_LOG_WARNING,
+ "Duplicate MP Index IFD\n");
+ return explode ? AVERROR_INVALIDDATA : 0;
+ }
+
+ /* MP offsets are relative to the start of the TIFF header */
+ const int skip = sizeof("MPF");
+ return parse_mp_index(ctx, gb->buffer + skip, len - skip,
+ s->file_start + offset + skip);
+ }
+
+ return 0;
+}
+
+static int cmp_image_start(const void *a, const void *b)
+{
+ const MPFDemuxImage *ia = a, *ib = b;
+ if (ia->start != ib->start)
+ return FFDIFFSIGN(ia->start, ib->start);
+ return FFDIFFSIGN(ia->index, ib->index);
+}
+
+static int add_gain_map_group(AVFormatContext *ctx, const MPFDemuxImage *base,
+ const MPFDemuxImage *gain)
+{
+ if (gain == base) {
+ av_log(ctx, AV_LOG_ERROR, "The first image cannot be a gain map\n");
+ return AVERROR_INVALIDDATA;
+ }
+
+ AVStreamGroup *stg = avformat_stream_group_create(ctx, AV_STREAM_GROUP_PARAMS_GAIN_MAP, NULL);
+ if (!stg)
+ return AVERROR(ENOMEM);
+
+ int ret;
+ if ((ret = avformat_stream_group_add_stream(stg, base->st)) < 0 ||
+ (ret = avformat_stream_group_add_stream(stg, gain->st)) < 0)
+ return ret;
+
+ stg->params.layered_video->el_index = stg->nb_entries - 1;
+ return 0;
+}
+
+static int mpf_read_header(AVFormatContext *ctx)
+{
+ MPFDemuxContext *s = ctx->priv_data;
+ int ret;
+
+ s->first_img = -1;
+ s->initial = av_packet_alloc();
+ if (!s->initial)
+ return AVERROR(ENOMEM);
+
+ s->file_start = avio_tell(ctx->pb);
+ if ((ret = grow_header(ctx, 2)) < 0)
+ return ret;
+ if (AV_RB16(s->initial->data) != (0xFF00 | SOI))
+ return AVERROR_INVALIDDATA;
+
+ /* Walk the JPEG marker sections up until the start of entropy coded data */
+ for (;;) {
+ const int pos = s->initial->size;
+ if ((ret = grow_header(ctx, 1)) < 0)
+ return ret;
+ uint8_t marker = s->initial->data[ret];
+ if (marker != 0xFF) {
+ av_log(ctx, AV_LOG_ERROR, "Malformed JPEG marker at 0x%x\n", pos);
+ return AVERROR_INVALIDDATA;
+ }
+
+ while (marker == 0xFF) {
+ if ((ret = grow_header(ctx, 1)) < 0)
+ return ret;
+ marker = s->initial->data[ret];
+ }
+
+ if (marker == SOS || marker == TEM || (marker >= RST0 && marker <= EOI))
+ break; /* start of entropy coded data */
+
+ if ((ret = grow_header(ctx, 2)) < 0)
+ return ret;
+ const int len = AV_RB16(s->initial->data + ret) - 2;
+ if (len < 0) {
+ av_log(ctx, AV_LOG_ERROR, "Invalid marker segment at 0x%x\n", pos);
+ return AVERROR_INVALIDDATA;
+ }
+
+ if ((ret = grow_header(ctx, len)) < 0)
+ return ret;
+ if (marker == APP2 && (ret = parse_app2(ctx, ret, len)) < 0)
+ return ret;
+ }
+
+ if (!s->images) {
+ /* Not an MPF file, just return as-is to the underlying decoder */
+ const int64_t file_size = avio_size(ctx->pb);
+ s->passthrough = file_size <= s->file_start;
+ s->first_img = 0;
+ s->nb_images = 1;
+ s->images = av_malloc(sizeof(*s->images));
+ if (!s->images)
+ return AVERROR(ENOMEM);
+ s->images[0] = (MPFDemuxImage){
+ .type = MPF_TYPE_PRIMARY,
+ .start = s->file_start,
+ .size = s->passthrough ? s->initial->size
+ : file_size - s->file_start,
+ };
+ }
+
+ if (s->first_img < 0) {
+ av_log(ctx, AV_LOG_ERROR, "No MP Entry for the first image\n");
+ return AVERROR_INVALIDDATA;
+ }
+
+ /* Compatibility hack for libultrahdr, which produces invalid MPF files
+ * whose gain maps have no MP type. Needed to support a large number
+ * of JPEG Ultra HDR images in the wild */
+ const MPFDemuxImage *first = &s->images[s->first_img];
+ if (ctx->strict_std_compliance < FF_COMPLIANCE_STRICT &&
+ s->has_gain_map && s->nb_images == 2)
+ {
+ MPFDemuxImage *other = &s->images[1 - s->first_img];
+ if (other->type == MPF_TYPE_UNDEFINED)
+ other->type = MPF_TYPE_GAIN_MAP;
+ }
+
+ for (int i = 0; i < s->nb_images; i++) {
+ s->images[i].st = avformat_new_stream(ctx, NULL);
+ if (!s->images[i].st)
+ return AVERROR(ENOMEM);
+ }
+
+ for (int i = 0; i < s->nb_images; i++) {
+ MPFDemuxImage *img = &s->images[i];
+ AVStream *st = img->st;
+
+ st->id = img->index;
+ st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+ st->codecpar->codec_id = AV_CODEC_ID_MJPEG;
+ if (img == first)
+ st->disposition |= AV_DISPOSITION_DEFAULT;
+ if (img->child || mpf_type_is_child(img->type))
+ st->disposition |= AV_DISPOSITION_DEPENDENT;
+ avpriv_set_pts_info(st, 64, 1, 25);
+ if (s->passthrough) /* we don't know the image size */
+ ffstream(st)->need_parsing = AVSTREAM_PARSE_FULL;
+
+ switch (img->type & MPF_TYPE_INFO_MASK) {
+ case MPF_TYPE_GAIN_MAP:
+ ret = add_gain_map_group(ctx, first, img);
+ if (ret < 0)
+ return ret;
+ break;
+ case MPF_TYPE_LARGE_THUMBNAIL:
+ case MPF_TYPE_MULTI_FRAME:
+ case MPF_TYPE_PRIMARY:
+ case MPF_TYPE_ORIGINAL:
+ /* no special handling */
+ break;
+ case MPF_TYPE_UNDEFINED:
+ default:
+ av_log(ctx, img->child ? AV_LOG_VERBOSE : AV_LOG_WARNING,
+ "Image %d: unhandled MP type 0x%06"PRIx32", treating as an "
+ "%s image\n", img->index, img->type,
+ img->child ? "unspecified dependent" : "unrelated");
+ break;
+ }
+ }
+
+ /* Sort the images array by file offset for mpf_read_packet() */
+ qsort(s->images, s->nb_images, sizeof(*s->images), cmp_image_start);
+ s->first_img = 0; /* by construction */
+
+ for (int i = 0; i < s->nb_images - 1; i++) {
+ MPFDemuxImage *img = &s->images[i], *next = &s->images[i + 1];
+ if (next->start < img->start + img->size) {
+ const int explode = ctx->error_recognition & AV_EF_EXPLODE;
+ av_log(ctx, explode ? AV_LOG_ERROR : AV_LOG_WARNING,
+ "Image %d overlaps image %d%s\n", img->index, next->index,
+ explode ? "" : ", truncating");
+ if (explode)
+ return AVERROR_INVALIDDATA;
+ img->size = next->start - img->start;
+ }
+ }
+
+ return 0;
+}
+
+static int mpf_read_packet(AVFormatContext *ctx, AVPacket *pkt)
+{
+ MPFDemuxContext *s = ctx->priv_data;
+ AVIOContext *pb = ctx->pb;
+ int ret;
+
+ if (s->cur_image >= s->nb_images) {
+ if (!s->passthrough)
+ return AVERROR_EOF;
+
+ if ((ret = av_get_packet(pb, pkt, 4096)) < 0)
+ return ret;
+ pkt->flags &= ~AV_PKT_FLAG_CORRUPT; /* short read at EOF */
+ pkt->stream_index = 0;
+ return 0;
+ }
+
+ const MPFDemuxImage *img = &s->images[s->cur_image++];
+ if (img->size > INT_MAX - AV_INPUT_BUFFER_PADDING_SIZE)
+ return AVERROR(ERANGE);
+
+ if (s->initial) {
+ av_packet_move_ref(pkt, s->initial);
+ av_packet_free(&s->initial);
+ av_shrink_packet(pkt, FFMIN(pkt->size, img->size));
+ } else {
+ int64_t pos = avio_seek(pb, img->start, SEEK_SET);
+ if (pos < 0) {
+ av_log(ctx, AV_LOG_ERROR, "Failed seeking to image %d at offset "
+ "%"PRId64"\n", img->st->id, img->start);
+ return pos;
+ }
+ }
+
+ if (pkt->size < img->size) {
+ ret = av_append_packet(pb, pkt, img->size - pkt->size);
+ if (ret < 0 && !pkt->size)
+ return ret;
+ }
+
+ pkt->stream_index = img->st->index;
+ pkt->pts = pkt->dts = 0;
+ pkt->duration = 1;
+ pkt->flags |= AV_PKT_FLAG_KEY;
+ return 0;
+}
+
+static int mpf_read_seek(AVFormatContext *ctx, int stream_index,
+ int64_t timestamp, int flags)
+{
+ MPFDemuxContext *s = ctx->priv_data;
+ if (s->passthrough || timestamp != 0)
+ return -1;
+
+ /* Don't seek if the initial packet is still pending; since this would
+ * desynchronize the input stream otherwise */
+ if (!s->initial) {
+ int64_t pos = avio_seek(ctx->pb, s->images[0].start, SEEK_SET);
+ if (pos < 0)
+ return pos;
+ }
+
+ s->cur_image = 0;
+ return 0;
+}
+
+static int mpf_read_close(AVFormatContext *ctx)
+{
+ MPFDemuxContext *s = ctx->priv_data;
+ av_freep(&s->images);
+ av_packet_free(&s->initial);
+ return 0;
+}
+
+const FFInputFormat ff_jpeg_mpf_demuxer = {
+ .p.name = "jpeg_mpf",
+ .p.long_name = NULL_IF_CONFIG_SMALL("JPEG Multi-Picture Format"),
+ .p.extensions = "mpo",
+ .priv_data_size = sizeof(MPFDemuxContext),
+ .flags_internal = FF_INFMT_FLAG_INIT_CLEANUP | FF_INFMT_FLAG_ID3V2_AUTO,
+ .read_probe = mpf_probe,
+ .read_header = mpf_read_header,
+ .read_packet = mpf_read_packet,
+ .read_close = mpf_read_close,
+ .read_seek = mpf_read_seek,
+};
diff --git a/libavformat/version.h b/libavformat/version.h
index f45b935203..eaf74946de 100644
--- a/libavformat/version.h
+++ b/libavformat/version.h
@@ -31,8 +31,8 @@
#include "version_major.h"
-#define LIBAVFORMAT_VERSION_MINOR 14
-#define LIBAVFORMAT_VERSION_MICRO 102
+#define LIBAVFORMAT_VERSION_MINOR 15
+#define LIBAVFORMAT_VERSION_MICRO 100
#define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
LIBAVFORMAT_VERSION_MINOR, \