Repository: python-pillow/Pillow

Issue:
Error opening jpeg file
Maybe it is something wrong on my setup, but never seen that Pillow had problems with `jpeg` files.
Unfortunately currently has no free time to dive and debug/fix it myself.

I will just create an issue, feel free to close it, if after test there will be no problem found.

### What are your OS, Python and Pillow versions?

* OS: `macOS 14`
* Python: `3.11.6`
* Pillow: `10.2` / `10.3.0.dev0`

```python
from PIL import Image, ImageSequence

if __name__ == "__main__":
    i = Image.open("bad.jpeg")
    for b in ImageSequence.Iterator(i):
        print(b.getexif())  # SyntaxError: not a TIFF file (header b'http://n' not valid)
```

```python
from PIL import Image

if __name__ == "__main__":
    i = Image.open("bad.jpeg")
    i.show() # OSError: image file is truncated (664585 bytes not processed)
```

Problematic file(removed)


Discussion:
For `i.show()`, [loading truncated images](https://pillow.readthedocs.io/en/stable/reference/ImageFile.html#PIL.ImageFile.LOAD_TRUNCATED_IMAGES) is enough to fix this.
```python
from PIL import Image, ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True

if __name__ == "__main__":
    i = Image.open("bad.jpeg")
    i.show()
```

Also, as far as Pillow is concerned at least, this isn't a JPEG image, it's an MPO.


> For `i.show()`, [loading truncated images](https://pillow.readthedocs.io/en/stable/reference/ImageFile.html#PIL.ImageFile.LOAD_TRUNCATED_IMAGES) is enough to fix this.

I doubt the file is corrupted or truncated..
As I remember it is just and exported file from Apple "Photos" app
Looking at the EXIF matter, I take page 10 of the [MPO specification](https://web.archive.org/web/20190227081740/http://www.cipa.jp/std/documents/e/DC-007_E.pdf) to make that APP1 markers should contain EXIF data in MPO images. However, your MPO image does not. I've created #7884 to update Pillow to be flexible, allowing your code to output this instead -
```
{322: 512, 323: 512, 296: 2, 34665: 246, 271: 'Apple', 272: 'iPhone 13 Pro', 305: '16.2', 274: 1, 306: '2023:02:25 11:13:13', 531: 1, 282: 72.0, 283: 72.0, 316: 'iPhone 13 Pro'}
{322: 512, 323: 512, 296: 2, 34665: 246, 271: 'Apple', 272: 'iPhone 13 Pro', 305: '16.2', 274: 1, 306: '2023:02:25 11:13:13', 531: 1, 282: 72.0, 283: 72.0, 316: 'iPhone 13 Pro'}
{322: 512, 323: 512, 296: 2, 34665: 246, 271: 'Apple', 272: 'iPhone 13 Pro', 305: '16.2', 274: 1, 306: '2023:02:25 11:13:13', 531: 1, 282: 72.0, 283: 72.0, 316: 'iPhone 13 Pro'}
{322: 512, 323: 512, 296: 2, 34665: 246, 271: 'Apple', 272: 'iPhone 13 Pro', 305: '16.2', 274: 1, 306: '2023:02:25 11:13:13', 531: 1, 282: 72.0, 283: 72.0, 316: 'iPhone 13 Pro'}
{322: 512, 323: 512, 296: 2, 34665: 246, 271: 'Apple', 272: 'iPhone 13 Pro', 305: '16.2', 274: 1, 306: '2023:02:25 11:13:13', 531: 1, 282: 72.0, 283: 72.0, 316: 'iPhone 13 Pro'}
{322: 512, 323: 512, 296: 2, 34665: 246, 271: 'Apple', 272: 'iPhone 13 Pro', 305: '16.2', 274: 1, 306: '2023:02:25 11:13:13', 531: 1, 282: 72.0, 283: 72.0, 316: 'iPhone 13 Pro'}
{322: 512, 323: 512, 296: 2, 34665: 246, 271: 'Apple', 272: 'iPhone 13 Pro', 305: '16.2', 274: 1, 306: '2023:02:25 11:13:13', 531: 1, 282: 72.0, 283: 72.0, 316: 'iPhone 13 Pro'}
{322: 512, 323: 512, 296: 2, 34665: 246, 271: 'Apple', 272: 'iPhone 13 Pro', 305: '16.2', 274: 1, 306: '2023:02:25 11:13:13', 531: 1, 282: 72.0, 283: 72.0, 316: 'iPhone 13 Pro'}
```
Thank you, it was mainly the Exif reading code that worried me. Issue can be closed)

Resolving patch:
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -24,14 +24,11 @@
 import struct
 
 from . import (
-    ExifTags,
     Image,
-    ImageFile,
     ImageSequence,
     JpegImagePlugin,
     TiffImagePlugin,
 )
-from ._binary import i16be as i16
 from ._binary import o32le
 
 
@@ -109,7 +106,6 @@ def _open(self):
         self._after_jpeg_open()
 
     def _after_jpeg_open(self, mpheader=None):
-        self._initial_size = self.size
         self.mpinfo = mpheader if mpheader is not None else self._getmp()
         self.n_frames = self.mpinfo[0xB001]
         self.__mpoffsets = [
@@ -137,27 +133,20 @@ def seek(self, frame):
         self.fp = self._fp
         self.offset = self.__mpoffsets[frame]
 
+        original_exif = self.info.get("exif")
+        if "exif" in self.info:
+            del self.info["exif"]
+
         self.fp.seek(self.offset + 2)  # skip SOI marker
-        segment = self.fp.read(2)
-        if not segment:
+        if not self.fp.read(2):
             msg = "No data found for frame"
             raise ValueError(msg)
-        self._size = self._initial_size
-        if i16(segment) == 0xFFE1:  # APP1
-            n = i16(self.fp.read(2)) - 2
-            self.info["exif"] = ImageFile._safe_read(self.fp, n)
-            self._reload_exif()
-
-            mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"]
-            if mptype.startswith("Large Thumbnail"):
-                exif = self.getexif().get_ifd(ExifTags.IFD.Exif)
-                if 40962 in exif and 40963 in exif:
-                    self._size = (exif[40962], exif[40963])
-        elif "exif" in self.info:
-            del self.info["exif"]
+        self.fp.seek(self.offset)
+        JpegImagePlugin.JpegImageFile._open(self)
+        if self.info.get("exif") != original_exif:
             self._reload_exif()
 
-        self.tile = [("jpeg", (0, 0) + self.size, self.offset, (self.mode, ""))]
+        self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])]
         self.__frame = frame
 
     def tell(self):


Test patch:
diff --git a/Tests/images/sugarshack_frame_size.mpo b/Tests/images/sugarshack_frame_size.mpo
Binary files a/Tests/images/sugarshack_frame_size.mpo and b/Tests/images/sugarshack_frame_size.mpo differ
diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -93,7 +93,7 @@ def test_exif(test_file: str) -> None:
 
 def test_frame_size() -> None:
     # This image has been hexedited to contain a different size
-    # in the EXIF data of the second frame
+    # in the SOF marker of the second frame
     with Image.open("Tests/images/sugarshack_frame_size.mpo") as im:
         assert im.size == (640, 480)