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
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 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)
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])
77 ifd = TiffImagePlugin.ImageFileDirectory_v2()
78 ifd[0xB000] = b"0100"
79 ifd[0xB001] = len(offsets)
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
94 fp.seek(mpf_offset)
95 fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8))
96 fp.seek(0, os.SEEK_END)
99##
100# Image plugin for MPO images.
103class MpoImageFile(JpegImagePlugin.JpegImageFile):
104 format = "MPO"
105 format_description = "MPO (CIPA DC-007)"
106 _close_exclusive_fp_after_loading = False
108 def _open(self) -> None:
109 assert self.fp is not None
110 self.fp.seek(0) # prep the fp in order to pass the JPEG test
111 JpegImagePlugin.JpegImageFile._open(self)
112 self._after_jpeg_open()
114 def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None:
115 self.mpinfo = mpheader if mpheader is not None else self._getmp()
116 if self.mpinfo is None:
117 msg = "Image appears to be a malformed MPO file"
118 raise ValueError(msg)
119 self.n_frames = self.mpinfo[0xB001]
120 self.__mpoffsets = [
121 mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
122 ]
123 self.__mpoffsets[0] = 0
124 # Note that the following assertion will only be invalid if something
125 # gets broken within JpegImagePlugin.
126 assert self.n_frames == len(self.__mpoffsets)
127 del self.info["mpoffset"] # no longer needed
128 self.is_animated = self.n_frames > 1
129 assert self.fp is not None
130 self._fp = self.fp # FIXME: hack
131 self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame
132 self.__frame = 0
133 self.offset = 0
134 # for now we can only handle reading and individual frame extraction
135 self.readonly = 1
137 def load_seek(self, pos: int) -> None:
138 if isinstance(self._fp, DeferredError):
139 raise self._fp.ex
140 self._fp.seek(pos)
142 def seek(self, frame: int) -> None:
143 if not self._seek_check(frame):
144 return
145 if isinstance(self._fp, DeferredError):
146 raise self._fp.ex
147 self.fp = self._fp
148 self.offset = self.__mpoffsets[frame]
150 original_exif = self.info.get("exif")
151 if "exif" in self.info:
152 del self.info["exif"]
154 self.fp.seek(self.offset + 2) # skip SOI marker
155 if not self.fp.read(2):
156 msg = "No data found for frame"
157 raise ValueError(msg)
158 self.fp.seek(self.offset)
159 JpegImagePlugin.JpegImageFile._open(self)
160 if self.info.get("exif") != original_exif:
161 self._reload_exif()
163 self.tile = [
164 ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])
165 ]
166 self.__frame = frame
168 def tell(self) -> int:
169 return self.__frame
171 @staticmethod
172 def adopt(
173 jpeg_instance: JpegImagePlugin.JpegImageFile,
174 mpheader: dict[int, Any] | None = None,
175 ) -> MpoImageFile:
176 """
177 Transform the instance of JpegImageFile into
178 an instance of MpoImageFile.
179 After the call, the JpegImageFile is extended
180 to be an MpoImageFile.
182 This is essentially useful when opening a JPEG
183 file that reveals itself as an MPO, to avoid
184 double call to _open.
185 """
186 jpeg_instance.__class__ = MpoImageFile
187 mpo_instance = cast(MpoImageFile, jpeg_instance)
188 mpo_instance._after_jpeg_open(mpheader)
189 return mpo_instance
192# ---------------------------------------------------------------------
193# Registry stuff
195# Note that since MPO shares a factory with JPEG, we do not need to do a
196# separate registration for it here.
197# Image.register_open(MpoImageFile.format,
198# JpegImagePlugin.jpeg_factory, _accept)
199Image.register_save(MpoImageFile.format, _save)
200Image.register_save_all(MpoImageFile.format, _save_all)
202Image.register_extension(MpoImageFile.format, ".mpo")
204Image.register_mime(MpoImageFile.format, "image/mpo")