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

113 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 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 self._fp = self.fp # FIXME: hack 

129 self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame 

130 self.__frame = 0 

131 self.offset = 0 

132 # for now we can only handle reading and individual frame extraction 

133 self.readonly = 1 

134 

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

136 if isinstance(self._fp, DeferredError): 

137 raise self._fp.ex 

138 self._fp.seek(pos) 

139 

140 def seek(self, frame: int) -> None: 

141 if not self._seek_check(frame): 

142 return 

143 if isinstance(self._fp, DeferredError): 

144 raise self._fp.ex 

145 self.fp = self._fp 

146 self.offset = self.__mpoffsets[frame] 

147 

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

149 if "exif" in self.info: 

150 del self.info["exif"] 

151 

152 self.fp.seek(self.offset + 2) # skip SOI marker 

153 if not self.fp.read(2): 

154 msg = "No data found for frame" 

155 raise ValueError(msg) 

156 self.fp.seek(self.offset) 

157 JpegImagePlugin.JpegImageFile._open(self) 

158 if self.info.get("exif") != original_exif: 

159 self._reload_exif() 

160 

161 self.tile = [ 

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

163 ] 

164 self.__frame = frame 

165 

166 def tell(self) -> int: 

167 return self.__frame 

168 

169 @staticmethod 

170 def adopt( 

171 jpeg_instance: JpegImagePlugin.JpegImageFile, 

172 mpheader: dict[int, Any] | None = None, 

173 ) -> MpoImageFile: 

174 """ 

175 Transform the instance of JpegImageFile into 

176 an instance of MpoImageFile. 

177 After the call, the JpegImageFile is extended 

178 to be an MpoImageFile. 

179 

180 This is essentially useful when opening a JPEG 

181 file that reveals itself as an MPO, to avoid 

182 double call to _open. 

183 """ 

184 jpeg_instance.__class__ = MpoImageFile 

185 mpo_instance = cast(MpoImageFile, jpeg_instance) 

186 mpo_instance._after_jpeg_open(mpheader) 

187 return mpo_instance 

188 

189 

190# --------------------------------------------------------------------- 

191# Registry stuff 

192 

193# Note that since MPO shares a factory with JPEG, we do not need to do a 

194# separate registration for it here. 

195# Image.register_open(MpoImageFile.format, 

196# JpegImagePlugin.jpeg_factory, _accept) 

197Image.register_save(MpoImageFile.format, _save) 

198Image.register_save_all(MpoImageFile.format, _save_all) 

199 

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

201 

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