Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/MpoImagePlugin.py: 40%

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

109 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 itertools 

23import os 

24import struct 

25from typing import IO, Any, cast 

26 

27from . import ( 

28 Image, 

29 ImageFile, 

30 ImageSequence, 

31 JpegImagePlugin, 

32 TiffImagePlugin, 

33) 

34from ._binary import o32le 

35from ._util import DeferredError 

36 

37 

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

39 JpegImagePlugin._save(im, fp, filename) 

40 

41 

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

43 append_images = im.encoderinfo.get("append_images", []) 

44 if not append_images and not getattr(im, "is_animated", False): 

45 _save(im, fp, filename) 

46 return 

47 

48 mpf_offset = 28 

49 offsets: list[int] = [] 

50 for imSequence in itertools.chain([im], append_images): 

51 for im_frame in ImageSequence.Iterator(imSequence): 

52 if not offsets: 

53 # APP2 marker 

54 im_frame.encoderinfo["extra"] = ( 

55 b"\xff\xe2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 

56 ) 

57 exif = im_frame.encoderinfo.get("exif") 

58 if isinstance(exif, Image.Exif): 

59 exif = exif.tobytes() 

60 im_frame.encoderinfo["exif"] = exif 

61 if exif: 

62 mpf_offset += 4 + len(exif) 

63 

64 JpegImagePlugin._save(im_frame, fp, filename) 

65 offsets.append(fp.tell()) 

66 else: 

67 im_frame.save(fp, "JPEG") 

68 offsets.append(fp.tell() - offsets[-1]) 

69 

70 ifd = TiffImagePlugin.ImageFileDirectory_v2() 

71 ifd[0xB000] = b"0100" 

72 ifd[0xB001] = len(offsets) 

73 

74 mpentries = b"" 

75 data_offset = 0 

76 for i, size in enumerate(offsets): 

77 if i == 0: 

78 mptype = 0x030000 # Baseline MP Primary Image 

79 else: 

80 mptype = 0x000000 # Undefined 

81 mpentries += struct.pack("<LLLHH", mptype, size, data_offset, 0, 0) 

82 if i == 0: 

83 data_offset -= mpf_offset 

84 data_offset += size 

85 ifd[0xB002] = mpentries 

86 

87 fp.seek(mpf_offset) 

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

89 fp.seek(0, os.SEEK_END) 

90 

91 

92## 

93# Image plugin for MPO images. 

94 

95 

96class MpoImageFile(JpegImagePlugin.JpegImageFile): 

97 format = "MPO" 

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

99 _close_exclusive_fp_after_loading = False 

100 

101 def _open(self) -> None: 

102 self.fp.seek(0) # prep the fp in order to pass the JPEG test 

103 JpegImagePlugin.JpegImageFile._open(self) 

104 self._after_jpeg_open() 

105 

106 def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None: 

107 self.mpinfo = mpheader if mpheader is not None else self._getmp() 

108 if self.mpinfo is None: 

109 msg = "Image appears to be a malformed MPO file" 

110 raise ValueError(msg) 

111 self.n_frames = self.mpinfo[0xB001] 

112 self.__mpoffsets = [ 

113 mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002] 

114 ] 

115 self.__mpoffsets[0] = 0 

116 # Note that the following assertion will only be invalid if something 

117 # gets broken within JpegImagePlugin. 

118 assert self.n_frames == len(self.__mpoffsets) 

119 del self.info["mpoffset"] # no longer needed 

120 self.is_animated = self.n_frames > 1 

121 self._fp = self.fp # FIXME: hack 

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

123 self.__frame = 0 

124 self.offset = 0 

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

126 self.readonly = 1 

127 

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

129 if isinstance(self._fp, DeferredError): 

130 raise self._fp.ex 

131 self._fp.seek(pos) 

132 

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

134 if not self._seek_check(frame): 

135 return 

136 if isinstance(self._fp, DeferredError): 

137 raise self._fp.ex 

138 self.fp = self._fp 

139 self.offset = self.__mpoffsets[frame] 

140 

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

142 if "exif" in self.info: 

143 del self.info["exif"] 

144 

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

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

147 msg = "No data found for frame" 

148 raise ValueError(msg) 

149 self.fp.seek(self.offset) 

150 JpegImagePlugin.JpegImageFile._open(self) 

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

152 self._reload_exif() 

153 

154 self.tile = [ 

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

156 ] 

157 self.__frame = frame 

158 

159 def tell(self) -> int: 

160 return self.__frame 

161 

162 @staticmethod 

163 def adopt( 

164 jpeg_instance: JpegImagePlugin.JpegImageFile, 

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

166 ) -> MpoImageFile: 

167 """ 

168 Transform the instance of JpegImageFile into 

169 an instance of MpoImageFile. 

170 After the call, the JpegImageFile is extended 

171 to be an MpoImageFile. 

172 

173 This is essentially useful when opening a JPEG 

174 file that reveals itself as an MPO, to avoid 

175 double call to _open. 

176 """ 

177 jpeg_instance.__class__ = MpoImageFile 

178 mpo_instance = cast(MpoImageFile, jpeg_instance) 

179 mpo_instance._after_jpeg_open(mpheader) 

180 return mpo_instance 

181 

182 

183# --------------------------------------------------------------------- 

184# Registry stuff 

185 

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

187# separate registration for it here. 

188# Image.register_open(MpoImageFile.format, 

189# JpegImagePlugin.jpeg_factory, _accept) 

190Image.register_save(MpoImageFile.format, _save) 

191Image.register_save_all(MpoImageFile.format, _save_all) 

192 

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

194 

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