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

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

67 

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

75 

76 ifd = TiffImagePlugin.ImageFileDirectory_v2() 

77 ifd[0xB000] = b"0100" 

78 ifd[0xB001] = len(offsets) 

79 

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 

92 

93 fp.seek(mpf_offset) 

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

95 fp.seek(0, os.SEEK_END) 

96 

97 

98## 

99# Image plugin for MPO images. 

100 

101 

102class MpoImageFile(JpegImagePlugin.JpegImageFile): 

103 format = "MPO" 

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

105 _close_exclusive_fp_after_loading = False 

106 

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

112 

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 

135 

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

137 if isinstance(self._fp, DeferredError): 

138 raise self._fp.ex 

139 self._fp.seek(pos) 

140 

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] 

148 

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

150 if "exif" in self.info: 

151 del self.info["exif"] 

152 

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

161 

162 self.tile = [ 

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

164 ] 

165 self.__frame = frame 

166 

167 def tell(self) -> int: 

168 return self.__frame 

169 

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. 

180 

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 

189 

190 

191# --------------------------------------------------------------------- 

192# Registry stuff 

193 

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) 

200 

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

202 

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