MpoImagePlugin.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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 itertools
  22. import os
  23. import struct
  24. from typing import IO
  25. from . import (
  26. Image,
  27. ImageSequence,
  28. JpegImagePlugin,
  29. TiffImagePlugin,
  30. )
  31. from ._binary import o32le
  32. def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
  33. JpegImagePlugin._save(im, fp, filename)
  34. def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
  35. append_images = im.encoderinfo.get("append_images", [])
  36. if not append_images and not getattr(im, "is_animated", False):
  37. _save(im, fp, filename)
  38. return
  39. mpf_offset = 28
  40. offsets: list[int] = []
  41. for imSequence in itertools.chain([im], append_images):
  42. for im_frame in ImageSequence.Iterator(imSequence):
  43. if not offsets:
  44. # APP2 marker
  45. im_frame.encoderinfo["extra"] = (
  46. b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82
  47. )
  48. exif = im_frame.encoderinfo.get("exif")
  49. if isinstance(exif, Image.Exif):
  50. exif = exif.tobytes()
  51. im_frame.encoderinfo["exif"] = exif
  52. if exif:
  53. mpf_offset += 4 + len(exif)
  54. JpegImagePlugin._save(im_frame, fp, filename)
  55. offsets.append(fp.tell())
  56. else:
  57. im_frame.save(fp, "JPEG")
  58. offsets.append(fp.tell() - offsets[-1])
  59. ifd = TiffImagePlugin.ImageFileDirectory_v2()
  60. ifd[0xB000] = b"0100"
  61. ifd[0xB001] = len(offsets)
  62. mpentries = b""
  63. data_offset = 0
  64. for i, size in enumerate(offsets):
  65. if i == 0:
  66. mptype = 0x030000 # Baseline MP Primary Image
  67. else:
  68. mptype = 0x000000 # Undefined
  69. mpentries += struct.pack("<LLLHH", mptype, size, data_offset, 0, 0)
  70. if i == 0:
  71. data_offset -= mpf_offset
  72. data_offset += size
  73. ifd[0xB002] = mpentries
  74. fp.seek(mpf_offset)
  75. fp.write(b"II\x2A\x00" + o32le(8) + ifd.tobytes(8))
  76. fp.seek(0, os.SEEK_END)
  77. ##
  78. # Image plugin for MPO images.
  79. class MpoImageFile(JpegImagePlugin.JpegImageFile):
  80. format = "MPO"
  81. format_description = "MPO (CIPA DC-007)"
  82. _close_exclusive_fp_after_loading = False
  83. def _open(self) -> None:
  84. self.fp.seek(0) # prep the fp in order to pass the JPEG test
  85. JpegImagePlugin.JpegImageFile._open(self)
  86. self._after_jpeg_open()
  87. def _after_jpeg_open(self, mpheader=None):
  88. self.mpinfo = mpheader if mpheader is not None else self._getmp()
  89. self.n_frames = self.mpinfo[0xB001]
  90. self.__mpoffsets = [
  91. mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
  92. ]
  93. self.__mpoffsets[0] = 0
  94. # Note that the following assertion will only be invalid if something
  95. # gets broken within JpegImagePlugin.
  96. assert self.n_frames == len(self.__mpoffsets)
  97. del self.info["mpoffset"] # no longer needed
  98. self.is_animated = self.n_frames > 1
  99. self._fp = self.fp # FIXME: hack
  100. self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame
  101. self.__frame = 0
  102. self.offset = 0
  103. # for now we can only handle reading and individual frame extraction
  104. self.readonly = 1
  105. def load_seek(self, pos: int) -> None:
  106. self._fp.seek(pos)
  107. def seek(self, frame: int) -> None:
  108. if not self._seek_check(frame):
  109. return
  110. self.fp = self._fp
  111. self.offset = self.__mpoffsets[frame]
  112. original_exif = self.info.get("exif")
  113. if "exif" in self.info:
  114. del self.info["exif"]
  115. self.fp.seek(self.offset + 2) # skip SOI marker
  116. if not self.fp.read(2):
  117. msg = "No data found for frame"
  118. raise ValueError(msg)
  119. self.fp.seek(self.offset)
  120. JpegImagePlugin.JpegImageFile._open(self)
  121. if self.info.get("exif") != original_exif:
  122. self._reload_exif()
  123. self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])]
  124. self.__frame = frame
  125. def tell(self) -> int:
  126. return self.__frame
  127. @staticmethod
  128. def adopt(jpeg_instance, mpheader=None):
  129. """
  130. Transform the instance of JpegImageFile into
  131. an instance of MpoImageFile.
  132. After the call, the JpegImageFile is extended
  133. to be an MpoImageFile.
  134. This is essentially useful when opening a JPEG
  135. file that reveals itself as an MPO, to avoid
  136. double call to _open.
  137. """
  138. jpeg_instance.__class__ = MpoImageFile
  139. jpeg_instance._after_jpeg_open(mpheader)
  140. return jpeg_instance
  141. # ---------------------------------------------------------------------
  142. # Registry stuff
  143. # Note that since MPO shares a factory with JPEG, we do not need to do a
  144. # separate registration for it here.
  145. # Image.register_open(MpoImageFile.format,
  146. # JpegImagePlugin.jpeg_factory, _accept)
  147. Image.register_save(MpoImageFile.format, _save)
  148. Image.register_save_all(MpoImageFile.format, _save_all)
  149. Image.register_extension(MpoImageFile.format, ".mpo")
  150. Image.register_mime(MpoImageFile.format, "image/mpo")