MpoImagePlugin.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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. from __future__ import annotations
  21. import os
  22. import struct
  23. from typing import IO, Any, cast
  24. from . import (
  25. Image,
  26. ImageFile,
  27. ImageSequence,
  28. JpegImagePlugin,
  29. TiffImagePlugin,
  30. )
  31. from ._binary import o32le
  32. from ._util import DeferredError
  33. def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
  34. JpegImagePlugin._save(im, fp, filename)
  35. def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
  36. append_images = im.encoderinfo.get("append_images", [])
  37. if not append_images and not getattr(im, "is_animated", False):
  38. _save(im, fp, filename)
  39. return
  40. mpf_offset = 28
  41. offsets: list[int] = []
  42. im_sequences = [im, *append_images]
  43. total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences)
  44. for im_sequence in im_sequences:
  45. for im_frame in ImageSequence.Iterator(im_sequence):
  46. if not offsets:
  47. # APP2 marker
  48. ifd_length = 66 + 16 * total
  49. im_frame.encoderinfo["extra"] = (
  50. b"\xff\xe2"
  51. + struct.pack(">H", 6 + ifd_length)
  52. + b"MPF\0"
  53. + b" " * ifd_length
  54. )
  55. if exif := im_frame.encoderinfo.get("exif"):
  56. if isinstance(exif, Image.Exif):
  57. exif = exif.tobytes()
  58. im_frame.encoderinfo["exif"] = exif
  59. mpf_offset += 4 + len(exif)
  60. JpegImagePlugin._save(im_frame, fp, filename)
  61. offsets.append(fp.tell())
  62. else:
  63. encoderinfo = im_frame._attach_default_encoderinfo(im)
  64. im_frame.save(fp, "JPEG")
  65. im_frame.encoderinfo = encoderinfo
  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) -> None:
  92. assert self.fp is not None
  93. self.fp.seek(0) # prep the fp in order to pass the JPEG test
  94. JpegImagePlugin.JpegImageFile._open(self)
  95. self._after_jpeg_open()
  96. def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None:
  97. self.mpinfo = mpheader if mpheader is not None else self._getmp()
  98. if self.mpinfo is None:
  99. msg = "Image appears to be a malformed MPO file"
  100. raise ValueError(msg)
  101. self.n_frames = self.mpinfo[0xB001]
  102. self.__mpoffsets = [
  103. mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
  104. ]
  105. self.__mpoffsets[0] = 0
  106. # Note that the following assertion will only be invalid if something
  107. # gets broken within JpegImagePlugin.
  108. assert self.n_frames == len(self.__mpoffsets)
  109. del self.info["mpoffset"] # no longer needed
  110. self.is_animated = self.n_frames > 1
  111. assert self.fp is not None
  112. self._fp = self.fp # FIXME: hack
  113. self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame
  114. self.__frame = 0
  115. self.offset = 0
  116. # for now we can only handle reading and individual frame extraction
  117. self.readonly = 1
  118. def load_seek(self, pos: int) -> None:
  119. if isinstance(self._fp, DeferredError):
  120. raise self._fp.ex
  121. self._fp.seek(pos)
  122. def seek(self, frame: int) -> None:
  123. if not self._seek_check(frame):
  124. return
  125. if isinstance(self._fp, DeferredError):
  126. raise self._fp.ex
  127. self.fp = self._fp
  128. self.offset = self.__mpoffsets[frame]
  129. original_exif = self.info.get("exif")
  130. if "exif" in self.info:
  131. del self.info["exif"]
  132. self.fp.seek(self.offset + 2) # skip SOI marker
  133. if not self.fp.read(2):
  134. msg = "No data found for frame"
  135. raise ValueError(msg)
  136. self.fp.seek(self.offset)
  137. JpegImagePlugin.JpegImageFile._open(self)
  138. if self.info.get("exif") != original_exif:
  139. self._reload_exif()
  140. self.tile = [
  141. ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])
  142. ]
  143. self.__frame = frame
  144. def tell(self) -> int:
  145. return self.__frame
  146. @staticmethod
  147. def adopt(
  148. jpeg_instance: JpegImagePlugin.JpegImageFile,
  149. mpheader: dict[int, Any] | None = None,
  150. ) -> MpoImageFile:
  151. """
  152. Transform the instance of JpegImageFile into
  153. an instance of MpoImageFile.
  154. After the call, the JpegImageFile is extended
  155. to be an MpoImageFile.
  156. This is essentially useful when opening a JPEG
  157. file that reveals itself as an MPO, to avoid
  158. double call to _open.
  159. """
  160. jpeg_instance.__class__ = MpoImageFile
  161. mpo_instance = cast(MpoImageFile, jpeg_instance)
  162. mpo_instance._after_jpeg_open(mpheader)
  163. return mpo_instance
  164. # ---------------------------------------------------------------------
  165. # Registry stuff
  166. # Note that since MPO shares a factory with JPEG, we do not need to do a
  167. # separate registration for it here.
  168. # Image.register_open(MpoImageFile.format,
  169. # JpegImagePlugin.jpeg_factory, _accept)
  170. Image.register_save(MpoImageFile.format, _save)
  171. Image.register_save_all(MpoImageFile.format, _save_all)
  172. Image.register_extension(MpoImageFile.format, ".mpo")
  173. Image.register_mime(MpoImageFile.format, "image/mpo")