Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pillow-10.4.0-py3.8-linux-x86_64.egg/PIL/MpoImagePlugin.py: 24%

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

99 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 

26 

27from . import ( 

28 Image, 

29 ImageSequence, 

30 JpegImagePlugin, 

31 TiffImagePlugin, 

32) 

33from ._binary import o32le 

34 

35 

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

37 JpegImagePlugin._save(im, fp, filename) 

38 

39 

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

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

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

43 _save(im, fp, filename) 

44 return 

45 

46 mpf_offset = 28 

47 offsets: list[int] = [] 

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

49 for im_frame in ImageSequence.Iterator(imSequence): 

50 if not offsets: 

51 # APP2 marker 

52 im_frame.encoderinfo["extra"] = ( 

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

54 ) 

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

56 if isinstance(exif, Image.Exif): 

57 exif = exif.tobytes() 

58 im_frame.encoderinfo["exif"] = exif 

59 if exif: 

60 mpf_offset += 4 + len(exif) 

61 

62 JpegImagePlugin._save(im_frame, fp, filename) 

63 offsets.append(fp.tell()) 

64 else: 

65 im_frame.save(fp, "JPEG") 

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

67 

68 ifd = TiffImagePlugin.ImageFileDirectory_v2() 

69 ifd[0xB000] = b"0100" 

70 ifd[0xB001] = len(offsets) 

71 

72 mpentries = b"" 

73 data_offset = 0 

74 for i, size in enumerate(offsets): 

75 if i == 0: 

76 mptype = 0x030000 # Baseline MP Primary Image 

77 else: 

78 mptype = 0x000000 # Undefined 

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

80 if i == 0: 

81 data_offset -= mpf_offset 

82 data_offset += size 

83 ifd[0xB002] = mpentries 

84 

85 fp.seek(mpf_offset) 

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

87 fp.seek(0, os.SEEK_END) 

88 

89 

90## 

91# Image plugin for MPO images. 

92 

93 

94class MpoImageFile(JpegImagePlugin.JpegImageFile): 

95 format = "MPO" 

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

97 _close_exclusive_fp_after_loading = False 

98 

99 def _open(self) -> None: 

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

101 JpegImagePlugin.JpegImageFile._open(self) 

102 self._after_jpeg_open() 

103 

104 def _after_jpeg_open(self, mpheader=None): 

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

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

107 self.__mpoffsets = [ 

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

109 ] 

110 self.__mpoffsets[0] = 0 

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

112 # gets broken within JpegImagePlugin. 

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

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

115 self.is_animated = self.n_frames > 1 

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

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

118 self.__frame = 0 

119 self.offset = 0 

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

121 self.readonly = 1 

122 

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

124 self._fp.seek(pos) 

125 

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

127 if not self._seek_check(frame): 

128 return 

129 self.fp = self._fp 

130 self.offset = self.__mpoffsets[frame] 

131 

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

133 if "exif" in self.info: 

134 del self.info["exif"] 

135 

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

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

138 msg = "No data found for frame" 

139 raise ValueError(msg) 

140 self.fp.seek(self.offset) 

141 JpegImagePlugin.JpegImageFile._open(self) 

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

143 self._reload_exif() 

144 

145 self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])] 

146 self.__frame = frame 

147 

148 def tell(self) -> int: 

149 return self.__frame 

150 

151 @staticmethod 

152 def adopt(jpeg_instance, mpheader=None): 

153 """ 

154 Transform the instance of JpegImageFile into 

155 an instance of MpoImageFile. 

156 After the call, the JpegImageFile is extended 

157 to be an MpoImageFile. 

158 

159 This is essentially useful when opening a JPEG 

160 file that reveals itself as an MPO, to avoid 

161 double call to _open. 

162 """ 

163 jpeg_instance.__class__ = MpoImageFile 

164 jpeg_instance._after_jpeg_open(mpheader) 

165 return jpeg_instance 

166 

167 

168# --------------------------------------------------------------------- 

169# Registry stuff 

170 

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

172# separate registration for it here. 

173# Image.register_open(MpoImageFile.format, 

174# JpegImagePlugin.jpeg_factory, _accept) 

175Image.register_save(MpoImageFile.format, _save) 

176Image.register_save_all(MpoImageFile.format, _save_all) 

177 

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

179 

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