Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
MpoImagePlugin.py204 linesDownload Raw Back to PIL
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 os
23import struct
24from typing import IO, Any, cast
25
26from . import (
27    Image,
28    ImageFile,
29    ImageSequence,
30    JpegImagePlugin,
31    TiffImagePlugin,
32)
33from ._binary import o32le
34from ._util import DeferredError
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    im_sequences = [im, *append_images]
50    total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences)
51    for im_sequence in im_sequences:
52        for im_frame in ImageSequence.Iterator(im_sequence):
53            if not offsets:
54                # APP2 marker
55                ifd_length = 66 + 16 * total
56                im_frame.encoderinfo["extra"] = (
57                    b"\xff\xe2"
58                    + struct.pack(">H", 6 + ifd_length)
59                    + b"MPF\0"
60                    + b" " * ifd_length
61                )
62                if exif := im_frame.encoderinfo.get("exif"):
63                    if isinstance(exif, Image.Exif):
64                        exif = exif.tobytes()
65                        im_frame.encoderinfo["exif"] = exif
66                    mpf_offset += 4 + len(exif)
67
68                JpegImagePlugin._save(im_frame, fp, filename)
69                offsets.append(fp.tell())
70            else:
71                encoderinfo = im_frame._attach_default_encoderinfo(im)
72                im_frame.save(fp, "JPEG")
73                im_frame.encoderinfo = encoderinfo
74                offsets.append(fp.tell() - offsets[-1])
75
76    ifd = TiffImagePlugin.ImageFileDirectory_v2()
77    ifd[0xB000] = b"0100"
78    ifd[0xB001] = len(offsets)
79
80    mpentries = b""
81    data_offset = 0
82    for i, size in enumerate(offsets):
83        if i == 0:
84            mptype = 0x030000  # Baseline MP Primary Image
85        else:
86            mptype = 0x000000  # Undefined
87        mpentries += struct.pack("<LLLHH", mptype, size, data_offset, 0, 0)
88        if i == 0:
89            data_offset -= mpf_offset
90        data_offset += size
91    ifd[0xB002] = mpentries
92
93    fp.seek(mpf_offset)
94    fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8))
95    fp.seek(0, os.SEEK_END)
96
97
98##
99# Image plugin for MPO images.
100
101
102class MpoImageFile(JpegImagePlugin.JpegImageFile):
103    format = "MPO"
104    format_description = "MPO (CIPA DC-007)"
105    _close_exclusive_fp_after_loading = False
106
107    def _open(self) -> None:
108        assert self.fp is not None
109        self.fp.seek(0)  # prep the fp in order to pass the JPEG test
110        JpegImagePlugin.JpegImageFile._open(self)
111        self._after_jpeg_open()
112
113    def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None:
114        self.mpinfo = mpheader if mpheader is not None else self._getmp()
115        if self.mpinfo is None:
116            msg = "Image appears to be a malformed MPO file"
117            raise ValueError(msg)
118        self.n_frames = self.mpinfo[0xB001]
119        self.__mpoffsets = [
120            mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
121        ]
122        self.__mpoffsets[0] = 0
123        # Note that the following assertion will only be invalid if something
124        # gets broken within JpegImagePlugin.
125        assert self.n_frames == len(self.__mpoffsets)
126        del self.info["mpoffset"]  # no longer needed
127        self.is_animated = self.n_frames > 1
128        assert self.fp is not None
129        self._fp = self.fp  # FIXME: hack
130        self._fp.seek(self.__mpoffsets[0])  # get ready to read first frame
131        self.__frame = 0
132        self.offset = 0
133        # for now we can only handle reading and individual frame extraction
134        self.readonly = 1
135
136    def load_seek(self, pos: int) -> None:
137        if isinstance(self._fp, DeferredError):
138            raise self._fp.ex
139        self._fp.seek(pos)
140
141    def seek(self, frame: int) -> None:
142        if not self._seek_check(frame):
143            return
144        if isinstance(self._fp, DeferredError):
145            raise self._fp.ex
146        self.fp = self._fp
147        self.offset = self.__mpoffsets[frame]
148
149        original_exif = self.info.get("exif")
150        if "exif" in self.info:
151            del self.info["exif"]
152
153        self.fp.seek(self.offset + 2)  # skip SOI marker
154        if not self.fp.read(2):
155            msg = "No data found for frame"
156            raise ValueError(msg)
157        self.fp.seek(self.offset)
158        JpegImagePlugin.JpegImageFile._open(self)
159        if self.info.get("exif") != original_exif:
160            self._reload_exif()
161
162        self.tile = [
163            ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])
164        ]
165        self.__frame = frame
166
167    def tell(self) -> int:
168        return self.__frame
169
170    @staticmethod
171    def adopt(
172        jpeg_instance: JpegImagePlugin.JpegImageFile,
173        mpheader: dict[int, Any] | None = None,
174    ) -> MpoImageFile:
175        """
176        Transform the instance of JpegImageFile into
177        an instance of MpoImageFile.
178        After the call, the JpegImageFile is extended
179        to be an MpoImageFile.
180
181        This is essentially useful when opening a JPEG
182        file that reveals itself as an MPO, to avoid
183        double call to _open.
184        """
185        jpeg_instance.__class__ = MpoImageFile
186        mpo_instance = cast(MpoImageFile, jpeg_instance)
187        mpo_instance._after_jpeg_open(mpheader)
188        return mpo_instance
189
190
191# ---------------------------------------------------------------------
192# Registry stuff
193
194# Note that since MPO shares a factory with JPEG, we do not need to do a
195# separate registration for it here.
196# Image.register_open(MpoImageFile.format,
197#                     JpegImagePlugin.jpeg_factory, _accept)
198Image.register_save(MpoImageFile.format, _save)
199Image.register_save_all(MpoImageFile.format, _save_all)
200
201Image.register_extension(MpoImageFile.format, ".mpo")
202
203Image.register_mime(MpoImageFile.format, "image/mpo")
204 
codekingpro/portable-devtools · Team Ai