MpoImagePlugin.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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. #
  20. import itertools
  21. import os
  22. import struct
  23. from . import (
  24. ExifTags,
  25. Image,
  26. ImageFile,
  27. ImageSequence,
  28. JpegImagePlugin,
  29. TiffImagePlugin,
  30. )
  31. from ._binary import i16be as i16
  32. from ._binary import o32le
  33. # def _accept(prefix):
  34. # return JpegImagePlugin._accept(prefix)
  35. def _save(im, fp, filename):
  36. JpegImagePlugin._save(im, fp, filename)
  37. def _save_all(im, fp, filename):
  38. append_images = im.encoderinfo.get("append_images", [])
  39. if not append_images:
  40. try:
  41. animated = im.is_animated
  42. except AttributeError:
  43. animated = False
  44. if not animated:
  45. _save(im, fp, filename)
  46. return
  47. mpf_offset = 28
  48. offsets = []
  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. 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. ifd = TiffImagePlugin.ImageFileDirectory_v2()
  68. ifd[0xB000] = b"0100"
  69. ifd[0xB001] = len(offsets)
  70. mpentries = b""
  71. data_offset = 0
  72. for i, size in enumerate(offsets):
  73. if i == 0:
  74. mptype = 0x030000 # Baseline MP Primary Image
  75. else:
  76. mptype = 0x000000 # Undefined
  77. mpentries += struct.pack("<LLLHH", mptype, size, data_offset, 0, 0)
  78. if i == 0:
  79. data_offset -= mpf_offset
  80. data_offset += size
  81. ifd[0xB002] = mpentries
  82. fp.seek(mpf_offset)
  83. fp.write(b"II\x2A\x00" + o32le(8) + ifd.tobytes(8))
  84. fp.seek(0, os.SEEK_END)
  85. ##
  86. # Image plugin for MPO images.
  87. class MpoImageFile(JpegImagePlugin.JpegImageFile):
  88. format = "MPO"
  89. format_description = "MPO (CIPA DC-007)"
  90. _close_exclusive_fp_after_loading = False
  91. def _open(self):
  92. self.fp.seek(0) # prep the fp in order to pass the JPEG test
  93. JpegImagePlugin.JpegImageFile._open(self)
  94. self._after_jpeg_open()
  95. def _after_jpeg_open(self, mpheader=None):
  96. self._initial_size = self.size
  97. self.mpinfo = mpheader if mpheader is not None else self._getmp()
  98. self.n_frames = self.mpinfo[0xB001]
  99. self.__mpoffsets = [
  100. mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
  101. ]
  102. self.__mpoffsets[0] = 0
  103. # Note that the following assertion will only be invalid if something
  104. # gets broken within JpegImagePlugin.
  105. assert self.n_frames == len(self.__mpoffsets)
  106. del self.info["mpoffset"] # no longer needed
  107. self.is_animated = self.n_frames > 1
  108. self._fp = self.fp # FIXME: hack
  109. self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame
  110. self.__frame = 0
  111. self.offset = 0
  112. # for now we can only handle reading and individual frame extraction
  113. self.readonly = 1
  114. def load_seek(self, pos):
  115. self._fp.seek(pos)
  116. def seek(self, frame):
  117. if not self._seek_check(frame):
  118. return
  119. self.fp = self._fp
  120. self.offset = self.__mpoffsets[frame]
  121. self.fp.seek(self.offset + 2) # skip SOI marker
  122. segment = self.fp.read(2)
  123. if not segment:
  124. msg = "No data found for frame"
  125. raise ValueError(msg)
  126. self._size = self._initial_size
  127. if i16(segment) == 0xFFE1: # APP1
  128. n = i16(self.fp.read(2)) - 2
  129. self.info["exif"] = ImageFile._safe_read(self.fp, n)
  130. self._reload_exif()
  131. mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"]
  132. if mptype.startswith("Large Thumbnail"):
  133. exif = self.getexif().get_ifd(ExifTags.IFD.Exif)
  134. if 40962 in exif and 40963 in exif:
  135. self._size = (exif[40962], exif[40963])
  136. elif "exif" in self.info:
  137. del self.info["exif"]
  138. self._reload_exif()
  139. self.tile = [("jpeg", (0, 0) + self.size, self.offset, (self.mode, ""))]
  140. self.__frame = frame
  141. def tell(self):
  142. return self.__frame
  143. @staticmethod
  144. def adopt(jpeg_instance, mpheader=None):
  145. """
  146. Transform the instance of JpegImageFile into
  147. an instance of MpoImageFile.
  148. After the call, the JpegImageFile is extended
  149. to be an MpoImageFile.
  150. This is essentially useful when opening a JPEG
  151. file that reveals itself as an MPO, to avoid
  152. double call to _open.
  153. """
  154. jpeg_instance.__class__ = MpoImageFile
  155. jpeg_instance._after_jpeg_open(mpheader)
  156. return jpeg_instance
  157. # ---------------------------------------------------------------------
  158. # Registry stuff
  159. # Note that since MPO shares a factory with JPEG, we do not need to do a
  160. # separate registration for it here.
  161. # Image.register_open(MpoImageFile.format,
  162. # JpegImagePlugin.jpeg_factory, _accept)
  163. Image.register_save(MpoImageFile.format, _save)
  164. Image.register_save_all(MpoImageFile.format, _save_all)
  165. Image.register_extension(MpoImageFile.format, ".mpo")
  166. Image.register_mime(MpoImageFile.format, "image/mpo")