1#
2# The Python Imaging Library.
3# $Id$
4#
5# MPO file handling
6#
7# See "Multi-Picture Format" (CIPA DC-007-Translation 2009, Standard of the
8# Camera & Imaging Products Association)
9#
10# The multi-picture object combines multiple JPEG images (with a modified EXIF
11# data format) into a single file. While it can theoretically be used much like
12# a GIF animation, it is commonly used to represent 3D photographs and is (as
13# of this writing) the most commonly used format by 3D cameras.
14#
15# History:
16# 2014-03-13 Feneric Created
17#
18# See the README file for information on usage and redistribution.
19#
20from __future__ import annotations
21
22import os
23import struct
24from typing import IO, Any, cast
25
26from . import (
27 Image,
28 ImageFile,
29 ImageSequence,
30 JpegImagePlugin,
31 TiffImagePlugin,
32)
33from ._binary import o32le
34from ._util import DeferredError
35
36
37def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
38 JpegImagePlugin._save(im, fp, filename)
39
40
41def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
42 append_images = im.encoderinfo.get("append_images", [])
43 if not append_images and not getattr(im, "is_animated", False):
44 _save(im, fp, filename)
45 return
46
47 mpf_offset = 28
48 offsets: list[int] = []
49 im_sequences = [im, *append_images]
50 total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences)
51 for im_sequence in im_sequences:
52 for im_frame in ImageSequence.Iterator(im_sequence):
53 if not offsets:
54 # APP2 marker
55 ifd_length = 66 + 16 * total
56 im_frame.encoderinfo["extra"] = (
57 b"\xff\xe2"
58 + struct.pack(">H", 6 + ifd_length)
59 + b"MPF\0"
60 + b" " * ifd_length
61 )
62 exif = im_frame.encoderinfo.get("exif")
63 if isinstance(exif, Image.Exif):
64 exif = exif.tobytes()
65 im_frame.encoderinfo["exif"] = exif
66 if exif:
67 mpf_offset += 4 + len(exif)
68
69 JpegImagePlugin._save(im_frame, fp, filename)
70 offsets.append(fp.tell())
71 else:
72 encoderinfo = im_frame._attach_default_encoderinfo(im)
73 im_frame.save(fp, "JPEG")
74 im_frame.encoderinfo = encoderinfo
75 offsets.append(fp.tell() - offsets[-1])
76
77 ifd = TiffImagePlugin.ImageFileDirectory_v2()
78 ifd[0xB000] = b"0100"
79 ifd[0xB001] = len(offsets)
80
81 mpentries = b""
82 data_offset = 0
83 for i, size in enumerate(offsets):
84 if i == 0:
85 mptype = 0x030000 # Baseline MP Primary Image
86 else:
87 mptype = 0x000000 # Undefined
88 mpentries += struct.pack("<LLLHH", mptype, size, data_offset, 0, 0)
89 if i == 0:
90 data_offset -= mpf_offset
91 data_offset += size
92 ifd[0xB002] = mpentries
93
94 fp.seek(mpf_offset)
95 fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8))
96 fp.seek(0, os.SEEK_END)
97
98
99##
100# Image plugin for MPO images.
101
102
103class MpoImageFile(JpegImagePlugin.JpegImageFile):
104 format = "MPO"
105 format_description = "MPO (CIPA DC-007)"
106 _close_exclusive_fp_after_loading = False
107
108 def _open(self) -> None:
109 self.fp.seek(0) # prep the fp in order to pass the JPEG test
110 JpegImagePlugin.JpegImageFile._open(self)
111 self._after_jpeg_open()
112
113 def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None:
114 self.mpinfo = mpheader if mpheader is not None else self._getmp()
115 if self.mpinfo is None:
116 msg = "Image appears to be a malformed MPO file"
117 raise ValueError(msg)
118 self.n_frames = self.mpinfo[0xB001]
119 self.__mpoffsets = [
120 mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
121 ]
122 self.__mpoffsets[0] = 0
123 # Note that the following assertion will only be invalid if something
124 # gets broken within JpegImagePlugin.
125 assert self.n_frames == len(self.__mpoffsets)
126 del self.info["mpoffset"] # no longer needed
127 self.is_animated = self.n_frames > 1
128 self._fp = self.fp # FIXME: hack
129 self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame
130 self.__frame = 0
131 self.offset = 0
132 # for now we can only handle reading and individual frame extraction
133 self.readonly = 1
134
135 def load_seek(self, pos: int) -> None:
136 if isinstance(self._fp, DeferredError):
137 raise self._fp.ex
138 self._fp.seek(pos)
139
140 def seek(self, frame: int) -> None:
141 if not self._seek_check(frame):
142 return
143 if isinstance(self._fp, DeferredError):
144 raise self._fp.ex
145 self.fp = self._fp
146 self.offset = self.__mpoffsets[frame]
147
148 original_exif = self.info.get("exif")
149 if "exif" in self.info:
150 del self.info["exif"]
151
152 self.fp.seek(self.offset + 2) # skip SOI marker
153 if not self.fp.read(2):
154 msg = "No data found for frame"
155 raise ValueError(msg)
156 self.fp.seek(self.offset)
157 JpegImagePlugin.JpegImageFile._open(self)
158 if self.info.get("exif") != original_exif:
159 self._reload_exif()
160
161 self.tile = [
162 ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])
163 ]
164 self.__frame = frame
165
166 def tell(self) -> int:
167 return self.__frame
168
169 @staticmethod
170 def adopt(
171 jpeg_instance: JpegImagePlugin.JpegImageFile,
172 mpheader: dict[int, Any] | None = None,
173 ) -> MpoImageFile:
174 """
175 Transform the instance of JpegImageFile into
176 an instance of MpoImageFile.
177 After the call, the JpegImageFile is extended
178 to be an MpoImageFile.
179
180 This is essentially useful when opening a JPEG
181 file that reveals itself as an MPO, to avoid
182 double call to _open.
183 """
184 jpeg_instance.__class__ = MpoImageFile
185 mpo_instance = cast(MpoImageFile, jpeg_instance)
186 mpo_instance._after_jpeg_open(mpheader)
187 return mpo_instance
188
189
190# ---------------------------------------------------------------------
191# Registry stuff
192
193# Note that since MPO shares a factory with JPEG, we do not need to do a
194# separate registration for it here.
195# Image.register_open(MpoImageFile.format,
196# JpegImagePlugin.jpeg_factory, _accept)
197Image.register_save(MpoImageFile.format, _save)
198Image.register_save_all(MpoImageFile.format, _save_all)
199
200Image.register_extension(MpoImageFile.format, ".mpo")
201
202Image.register_mime(MpoImageFile.format, "image/mpo")