codekingpro/portable-devtools
114k
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 if exif := im_frame.encoderinfo.get("exif"):
63 if isinstance(exif, Image.Exif):
64 exif = exif.tobytes()
65 im_frame.encoderinfo["exif"] = exif
66 mpf_offset += 4 + len(exif)
67
68 JpegImagePlugin._save(im_frame, fp, filename)
69 offsets.append(fp.tell())
70 else:
71 encoderinfo = im_frame._attach_default_encoderinfo(im)
72 im_frame.save(fp, "JPEG")
73 im_frame.encoderinfo = encoderinfo
74 offsets.append(fp.tell() - offsets[-1])
75
76 ifd = TiffImagePlugin.ImageFileDirectory_v2()
77 ifd[0xB000] = b"0100"
78 ifd[0xB001] = len(offsets)
79
80 mpentries = b""
81 data_offset = 0
82 for i, size in enumerate(offsets):
83 if i == 0:
84 mptype = 0x030000 # Baseline MP Primary Image
85 else:
86 mptype = 0x000000 # Undefined
87 mpentries += struct.pack("<LLLHH", mptype, size, data_offset, 0, 0)
88 if i == 0:
89 data_offset -= mpf_offset
90 data_offset += size
91 ifd[0xB002] = mpentries
92
93 fp.seek(mpf_offset)
94 fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8))
95 fp.seek(0, os.SEEK_END)
96
97
98##
99# Image plugin for MPO images.
100
101
102class MpoImageFile(JpegImagePlugin.JpegImageFile):
103 format = "MPO"
104 format_description = "MPO (CIPA DC-007)"
105 _close_exclusive_fp_after_loading = False
106
107 def _open(self) -> None:
108 assert self.fp is not 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 assert self.fp is not None
129 self._fp = self.fp # FIXME: hack
130 self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame
131 self.__frame = 0
132 self.offset = 0
133 # for now we can only handle reading and individual frame extraction
134 self.readonly = 1
135
136 def load_seek(self, pos: int) -> None:
137 if isinstance(self._fp, DeferredError):
138 raise self._fp.ex
139 self._fp.seek(pos)
140
141 def seek(self, frame: int) -> None:
142 if not self._seek_check(frame):
143 return
144 if isinstance(self._fp, DeferredError):
145 raise self._fp.ex
146 self.fp = self._fp
147 self.offset = self.__mpoffsets[frame]
148
149 original_exif = self.info.get("exif")
150 if "exif" in self.info:
151 del self.info["exif"]
152
153 self.fp.seek(self.offset + 2) # skip SOI marker
154 if not self.fp.read(2):
155 msg = "No data found for frame"
156 raise ValueError(msg)
157 self.fp.seek(self.offset)
158 JpegImagePlugin.JpegImageFile._open(self)
159 if self.info.get("exif") != original_exif:
160 self._reload_exif()
161
162 self.tile = [
163 ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])
164 ]
165 self.__frame = frame
166
167 def tell(self) -> int:
168 return self.__frame
169
170 @staticmethod
171 def adopt(
172 jpeg_instance: JpegImagePlugin.JpegImageFile,
173 mpheader: dict[int, Any] | None = None,
174 ) -> MpoImageFile:
175 """
176 Transform the instance of JpegImageFile into
177 an instance of MpoImageFile.
178 After the call, the JpegImageFile is extended
179 to be an MpoImageFile.
180
181 This is essentially useful when opening a JPEG
182 file that reveals itself as an MPO, to avoid
183 double call to _open.
184 """
185 jpeg_instance.__class__ = MpoImageFile
186 mpo_instance = cast(MpoImageFile, jpeg_instance)
187 mpo_instance._after_jpeg_open(mpheader)
188 return mpo_instance
189
190
191# ---------------------------------------------------------------------
192# Registry stuff
193
194# Note that since MPO shares a factory with JPEG, we do not need to do a
195# separate registration for it here.
196# Image.register_open(MpoImageFile.format,
197# JpegImagePlugin.jpeg_factory, _accept)
198Image.register_save(MpoImageFile.format, _save)
199Image.register_save_all(MpoImageFile.format, _save_all)
200
201Image.register_extension(MpoImageFile.format, ".mpo")
202
203Image.register_mime(MpoImageFile.format, "image/mpo")
204 