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

126 statements  

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 

22__lazy_modules__ = {"PIL._binary", "PIL._util", "struct"} 

23 

24import os 

25import struct 

26from io import BytesIO 

27from typing import cast 

28 

29from . import ( 

30 Image, 

31 ImageFile, 

32 ImageSequence, 

33 JpegImagePlugin, 

34 TiffImagePlugin, 

35) 

36from ._binary import o32le 

37from ._util import DeferredError 

38 

39TYPE_CHECKING = False 

40if TYPE_CHECKING: 

41 from typing import IO, Any 

42 

43 

44def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 

45 JpegImagePlugin._save(im, fp, filename) 

46 

47 

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 

53 

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 

74 

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 

87 

88 ifd = TiffImagePlugin.ImageFileDirectory_v2() 

89 ifd[0xB000] = b"0100" 

90 ifd[0xB001] = len(offsets) 

91 

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 

104 

105 fp.seek(mpf_offset) 

106 fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8)) 

107 fp.seek(0, os.SEEK_END) 

108 

109 

110## 

111# Image plugin for MPO images. 

112 

113 

114class MpoImageFile(JpegImagePlugin.JpegImageFile): 

115 format = "MPO" 

116 format_description = "MPO (CIPA DC-007)" 

117 _close_exclusive_fp_after_loading = False 

118 

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() 

124 

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 

147 

148 def load_seek(self, pos: int) -> None: 

149 if isinstance(self._fp, DeferredError): 

150 raise self._fp.ex 

151 self._fp.seek(pos) 

152 

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] 

160 

161 original_exif = self.info.get("exif") 

162 if "exif" in self.info: 

163 del self.info["exif"] 

164 

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() 

174 

175 self.tile = [ImageFile._Tile("jpeg", None, self.offset, self.tile[0][-1])] 

176 self.__frame = frame 

177 Image.Image.seek(self, frame) 

178 

179 def tell(self) -> int: 

180 return self.__frame 

181 

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. 

192 

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 

201 

202 

203# --------------------------------------------------------------------- 

204# Registry stuff 

205 

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) 

212 

213Image.register_extension(MpoImageFile.format, ".mpo") 

214 

215Image.register_mime(MpoImageFile.format, "image/mpo")