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

115 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 

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

68 

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

76 

77 ifd = TiffImagePlugin.ImageFileDirectory_v2() 

78 ifd[0xB000] = b"0100" 

79 ifd[0xB001] = len(offsets) 

80 

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 

93 

94 fp.seek(mpf_offset) 

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

96 fp.seek(0, os.SEEK_END) 

97 

98 

99## 

100# Image plugin for MPO images. 

101 

102 

103class MpoImageFile(JpegImagePlugin.JpegImageFile): 

104 format = "MPO" 

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

106 _close_exclusive_fp_after_loading = False 

107 

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

113 

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 

136 

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

138 if isinstance(self._fp, DeferredError): 

139 raise self._fp.ex 

140 self._fp.seek(pos) 

141 

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] 

149 

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

151 if "exif" in self.info: 

152 del self.info["exif"] 

153 

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

162 

163 self.tile = [ 

164 ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1]) 

165 ] 

166 self.__frame = frame 

167 

168 def tell(self) -> int: 

169 return self.__frame 

170 

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. 

181 

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 

190 

191 

192# --------------------------------------------------------------------- 

193# Registry stuff 

194 

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) 

201 

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

203 

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