Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/MpoImagePlugin.py: 38%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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
22__lazy_modules__ = {"PIL._binary", "PIL._util", "struct"}
24import os
25import struct
26from io import BytesIO
27from typing import cast
29from . import (
30 Image,
31 ImageFile,
32 ImageSequence,
33 JpegImagePlugin,
34 TiffImagePlugin,
35)
36from ._binary import o32le
37from ._util import DeferredError
39TYPE_CHECKING = False
40if TYPE_CHECKING:
41 from typing import IO, Any
44def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
45 JpegImagePlugin._save(im, fp, filename)
48def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
49 append_images = im.encoderinfo.get("append_images", [])
50 if not append_images and not getattr(im, "is_animated", False):
51 _save(im, fp, filename)
52 return
54 prev_offset = 0
55 offsets: list[int] = []
56 im_sequences = [im, *append_images]
57 total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences)
58 for im_sequence in im_sequences:
59 for im_frame in ImageSequence.Iterator(im_sequence):
60 if not offsets:
61 # APP2 marker
62 ifd_length = 66 + 16 * total
63 extra = (
64 b"\xff\xe2"
65 + struct.pack(">H", 6 + ifd_length)
66 + b"MPF\0"
67 + b" " * ifd_length
68 )
69 im_frame.encoderinfo["extra"] = extra
70 if exif := im_frame.encoderinfo.get("exif"):
71 if isinstance(exif, Image.Exif):
72 exif = exif.tobytes()
73 im_frame.encoderinfo["exif"] = exif
75 with BytesIO() as first_frame_fp:
76 JpegImagePlugin._save(im_frame, first_frame_fp, filename)
77 first_frame_data = first_frame_fp.getvalue()
78 fp.write(first_frame_data)
79 mpf_offset = first_frame_data.index(extra) + 8
80 else:
81 encoderinfo = im_frame._attach_default_encoderinfo(im)
82 im_frame.save(fp, "JPEG")
83 im_frame.encoderinfo = encoderinfo
84 offset = fp.tell()
85 offsets.append(offset - prev_offset)
86 prev_offset = offset
88 ifd = TiffImagePlugin.ImageFileDirectory_v2()
89 ifd[0xB000] = b"0100"
90 ifd[0xB001] = len(offsets)
92 mpentries = b""
93 data_offset = 0
94 for i, size in enumerate(offsets):
95 if i == 0:
96 mptype = 0x030000 # Baseline MP Primary Image
97 else:
98 mptype = 0x000000 # Undefined
99 mpentries += struct.pack("<LLLHH", mptype, size, data_offset, 0, 0)
100 if i == 0:
101 data_offset -= mpf_offset
102 data_offset += size
103 ifd[0xB002] = mpentries
105 fp.seek(mpf_offset)
106 fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8))
107 fp.seek(0, os.SEEK_END)
110##
111# Image plugin for MPO images.
114class MpoImageFile(JpegImagePlugin.JpegImageFile):
115 format = "MPO"
116 format_description = "MPO (CIPA DC-007)"
117 _close_exclusive_fp_after_loading = False
119 def _open(self) -> None:
120 assert self.fp is not None
121 self.fp.seek(0) # prep the fp in order to pass the JPEG test
122 JpegImagePlugin.JpegImageFile._open(self)
123 self._after_jpeg_open()
125 def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None:
126 self.mpinfo = mpheader if mpheader is not None else self._getmp()
127 if self.mpinfo is None:
128 msg = "Image appears to be a malformed MPO file"
129 raise ValueError(msg)
130 self.n_frames = self.mpinfo[0xB001]
131 self.__mpoffsets = [
132 mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
133 ]
134 self.__mpoffsets[0] = 0
135 # Note that the following assertion will only be invalid if something
136 # gets broken within JpegImagePlugin.
137 assert self.n_frames == len(self.__mpoffsets)
138 del self.info["mpoffset"] # no longer needed
139 self.is_animated = self.n_frames > 1
140 assert self.fp is not None
141 self._fp = self.fp # FIXME: hack
142 self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame
143 self.__frame = 0
144 self.offset = 0
145 # for now we can only handle reading and individual frame extraction
146 self.readonly = 1
148 def load_seek(self, pos: int) -> None:
149 if isinstance(self._fp, DeferredError):
150 raise self._fp.ex
151 self._fp.seek(pos)
153 def seek(self, frame: int) -> None:
154 if not self._seek_check(frame):
155 return
156 if isinstance(self._fp, DeferredError):
157 raise self._fp.ex
158 self.fp = self._fp
159 self.offset = self.__mpoffsets[frame]
161 original_exif = self.info.get("exif")
162 if "exif" in self.info:
163 del self.info["exif"]
165 self.fp.seek(self.offset + 2) # skip SOI marker
166 if not self.fp.read(2):
167 msg = "No data found for frame"
168 raise ValueError(msg)
169 self.fp.seek(self.offset)
170 JpegImagePlugin.JpegImageFile._open(self)
171 Image._decompression_bomb_check(self.size)
172 if self.info.get("exif") != original_exif:
173 self._reload_exif()
175 self.tile = [ImageFile._Tile("jpeg", None, self.offset, self.tile[0][-1])]
176 self.__frame = frame
177 Image.Image.seek(self, frame)
179 def tell(self) -> int:
180 return self.__frame
182 @staticmethod
183 def adopt(
184 jpeg_instance: JpegImagePlugin.JpegImageFile,
185 mpheader: dict[int, Any] | None = None,
186 ) -> MpoImageFile:
187 """
188 Transform the instance of JpegImageFile into
189 an instance of MpoImageFile.
190 After the call, the JpegImageFile is extended
191 to be an MpoImageFile.
193 This is essentially useful when opening a JPEG
194 file that reveals itself as an MPO, to avoid
195 double call to _open.
196 """
197 jpeg_instance.__class__ = MpoImageFile
198 mpo_instance = cast("MpoImageFile", jpeg_instance)
199 mpo_instance._after_jpeg_open(mpheader)
200 return mpo_instance
203# ---------------------------------------------------------------------
204# Registry stuff
206# Note that since MPO shares a factory with JPEG, we do not need to do a
207# separate registration for it here.
208# Image.register_open(MpoImageFile.format,
209# JpegImagePlugin.jpeg_factory, _accept)
210Image.register_save(MpoImageFile.format, _save)
211Image.register_save_all(MpoImageFile.format, _save_all)
213Image.register_extension(MpoImageFile.format, ".mpo")
215Image.register_mime(MpoImageFile.format, "image/mpo")