Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/pillow-11.0.0-py3.10-linux-x86_64.egg/PIL/MpoImagePlugin.py: 23%

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

103 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 

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 for imSequence in itertools.chain([im], append_images): 

50 for im_frame in ImageSequence.Iterator(imSequence): 

51 if not offsets: 

52 # APP2 marker 

53 im_frame.encoderinfo["extra"] = ( 

54 b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 

55 ) 

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

57 if isinstance(exif, Image.Exif): 

58 exif = exif.tobytes() 

59 im_frame.encoderinfo["exif"] = exif 

60 if exif: 

61 mpf_offset += 4 + len(exif) 

62 

63 JpegImagePlugin._save(im_frame, fp, filename) 

64 offsets.append(fp.tell()) 

65 else: 

66 im_frame.save(fp, "JPEG") 

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

68 

69 ifd = TiffImagePlugin.ImageFileDirectory_v2() 

70 ifd[0xB000] = b"0100" 

71 ifd[0xB001] = len(offsets) 

72 

73 mpentries = b"" 

74 data_offset = 0 

75 for i, size in enumerate(offsets): 

76 if i == 0: 

77 mptype = 0x030000 # Baseline MP Primary Image 

78 else: 

79 mptype = 0x000000 # Undefined 

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

81 if i == 0: 

82 data_offset -= mpf_offset 

83 data_offset += size 

84 ifd[0xB002] = mpentries 

85 

86 fp.seek(mpf_offset) 

87 fp.write(b"II\x2A\x00" + o32le(8) + ifd.tobytes(8)) 

88 fp.seek(0, os.SEEK_END) 

89 

90 

91## 

92# Image plugin for MPO images. 

93 

94 

95class MpoImageFile(JpegImagePlugin.JpegImageFile): 

96 format = "MPO" 

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

98 _close_exclusive_fp_after_loading = False 

99 

100 def _open(self) -> None: 

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

102 JpegImagePlugin.JpegImageFile._open(self) 

103 self._after_jpeg_open() 

104 

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

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

107 if self.mpinfo is None: 

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

109 raise ValueError(msg) 

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

111 self.__mpoffsets = [ 

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

113 ] 

114 self.__mpoffsets[0] = 0 

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

116 # gets broken within JpegImagePlugin. 

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

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

119 self.is_animated = self.n_frames > 1 

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

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

122 self.__frame = 0 

123 self.offset = 0 

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

125 self.readonly = 1 

126 

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

128 self._fp.seek(pos) 

129 

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

131 if not self._seek_check(frame): 

132 return 

133 self.fp = self._fp 

134 self.offset = self.__mpoffsets[frame] 

135 

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

137 if "exif" in self.info: 

138 del self.info["exif"] 

139 

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

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

142 msg = "No data found for frame" 

143 raise ValueError(msg) 

144 self.fp.seek(self.offset) 

145 JpegImagePlugin.JpegImageFile._open(self) 

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

147 self._reload_exif() 

148 

149 self.tile = [ 

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

151 ] 

152 self.__frame = frame 

153 

154 def tell(self) -> int: 

155 return self.__frame 

156 

157 @staticmethod 

158 def adopt( 

159 jpeg_instance: JpegImagePlugin.JpegImageFile, 

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

161 ) -> MpoImageFile: 

162 """ 

163 Transform the instance of JpegImageFile into 

164 an instance of MpoImageFile. 

165 After the call, the JpegImageFile is extended 

166 to be an MpoImageFile. 

167 

168 This is essentially useful when opening a JPEG 

169 file that reveals itself as an MPO, to avoid 

170 double call to _open. 

171 """ 

172 jpeg_instance.__class__ = MpoImageFile 

173 mpo_instance = cast(MpoImageFile, jpeg_instance) 

174 mpo_instance._after_jpeg_open(mpheader) 

175 return mpo_instance 

176 

177 

178# --------------------------------------------------------------------- 

179# Registry stuff 

180 

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

182# separate registration for it here. 

183# Image.register_open(MpoImageFile.format, 

184# JpegImagePlugin.jpeg_factory, _accept) 

185Image.register_save(MpoImageFile.format, _save) 

186Image.register_save_all(MpoImageFile.format, _save_all) 

187 

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

189 

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