Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/MpoImagePlugin.py: 39%
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
22import os
23import struct
24from typing import IO, Any, cast
26from . import (
27 Image,
28 ImageFile,
29 ImageSequence,
30 JpegImagePlugin,
31 TiffImagePlugin,
32)
33from ._binary import o32le
34from ._util import DeferredError
37def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
38 JpegImagePlugin._save(im, fp, filename)
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
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)
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])
76 ifd = TiffImagePlugin.ImageFileDirectory_v2()
77 ifd[0xB000] = b"0100"
78 ifd[0xB001] = len(offsets)
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
93 fp.seek(mpf_offset)
94 fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8))
95 fp.seek(0, os.SEEK_END)
98##
99# Image plugin for MPO images.
102class MpoImageFile(JpegImagePlugin.JpegImageFile):
103 format = "MPO"
104 format_description = "MPO (CIPA DC-007)"
105 _close_exclusive_fp_after_loading = False
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()
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
136 def load_seek(self, pos: int) -> None:
137 if isinstance(self._fp, DeferredError):
138 raise self._fp.ex
139 self._fp.seek(pos)
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]
149 original_exif = self.info.get("exif")
150 if "exif" in self.info:
151 del self.info["exif"]
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()
162 self.tile = [
163 ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])
164 ]
165 self.__frame = frame
167 def tell(self) -> int:
168 return self.__frame
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.
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
191# ---------------------------------------------------------------------
192# Registry stuff
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)
201Image.register_extension(MpoImageFile.format, ".mpo")
203Image.register_mime(MpoImageFile.format, "image/mpo")