Repository: python-pillow/Pillow

Issue:
No n_frames, or bad value for n_frames
When I feed the test file  flower2.jpg  into this code (from #1630)

```python
im = Image.open( fn )
imgcnt = im.n_frames
colors = im.getcolors( im.width * im.height )
if args.hist:
    for cnt, col in colors:
        allcolors[ col ] += cnt
    for iz in range( 1, imgcnt ):
        im = Image.open( fn ) # does getcolors implicitly close????
                              # without the open, get "seek of closed
                              # file" error on line below.
        im.seek( iz )
        colors = im.getcolors( im.width * im.height )
        for cnt, col in colors:
            allcolors[ col ] += cnt
```

I get "AttributeError: n_frames"

But other .jpg files do not get that error... this one: http://nevcal.com/temporary/20151110-105826gl.jpg has no problem with the attribute error on that line, but it gets a value of 2, apparently handles the seek OK, but dies in the second call to getcolors, with "OSError: image file is truncated (0 bytes not processed)".



Discussion:
I'd expect .jpg files to always return 1 as the  n_frames  attribute; some from some cameras have an embedded thumbnail in the metadata, but I don't think that is particularly standard?

Same environment as for #1630: Windows 10, Python 3.5.0, Pillow 3.0.0.

So I've added a hasattr workaround to avoid the Attribute error, but when the attribute exists, but is wrong? Harder to deal with...

Hi. I think the simple answer to your question is that 20151110-105826gl.jpg is not actually a JPEG file. It's an MPO, and Pillow reads it as such, giving it an `n_frames` attribute. This is a case of the file extension not reflecting the type of file.

JPEGs do not have multiple frames, and so currently don't have the `n_frames` attribute.

If you think that a change should be made to Pillow to clarify this somehow, feel free to say.

With regards to `OSError: image file is truncated (0 bytes not processed)`, the error can replicated with -

```python
from PIL import Image
im = Image.open('test.jpg')
im.seek(1)
im.load()
```

So it has nothing to do with n_frames. Is there any reason you think that the file isn't truncated as the error suggests?

Re: no n_frames for JPEG... I thought I saw some comments here and a pull request for adding n_frames to file types that didn't have them, for uniformity. Maybe that isn't released yet.

Re: MPO support.  I had never heard of MPO until you mentioned them. I found a little info on Wikipedia, and my camera is listed as one that uses them. The picture is one I took with my camera. All consistent so far. I copied the file from the camera. I didn't truncate it. I don't know why the camera would. I haven't found good documentation for MPO yet, that explains what EXIF fields distinguish them from JPG. None of my other JPG software has complained about them not being JPG.  This error may have nothing to do with n_frames, except that one shouldn't seek(1) if there are not n_frames, and n_frames produces 2.

Thanks again for your analysis.  I have some things to learn now... but there may be a bug, or at least a necessary workaround, for MPO files named JPG that come off standard consumer cameras.


Resolving patch:
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -18,7 +18,8 @@
 # See the README file for information on usage and redistribution.
 #
 
-from . import Image, JpegImagePlugin
+from . import Image, ImageFile, JpegImagePlugin
+from ._binary import i16be as i16
 
 # __version__ is deprecated and will be removed in a future version. Use
 # PIL.__version__ instead.
@@ -78,6 +79,20 @@ def seek(self, frame):
             return
         self.fp = self.__fp
         self.offset = self.__mpoffsets[frame]
+
+        self.fp.seek(self.offset + 2)  # skip SOI marker
+        if "parsed_exif" in self.info:
+            del self.info["parsed_exif"]
+        if i16(self.fp.read(2)) == 0xFFE1:  # APP1
+            n = i16(self.fp.read(2))-2
+            self.info["exif"] = ImageFile._safe_read(self.fp, n)
+
+            exif = self._getexif()
+            if 40962 in exif and 40963 in exif:
+                self._size = (exif[40962], exif[40963])
+        elif "exif" in self.info:
+            del self.info["exif"]
+
         self.tile = [
             ("jpeg", (0, 0) + self.size, self.offset, (self.mode, ""))
         ]


Test patch:
diff --git a/Tests/images/sugarshack_frame_size.mpo b/Tests/images/sugarshack_frame_size.mpo
new file mode 100644
Binary files /dev/null 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
@@ -55,6 +55,15 @@ def test_exif(self):
             self.assertEqual(info[296], 2)
             self.assertEqual(info[34665], 188)
 
+    def test_frame_size(self):
+        # This image has been hexedited to contain a different size
+        # in the EXIF data of the second frame
+        im = Image.open("Tests/images/sugarshack_frame_size.mpo")
+        self.assertEqual(im.size, (640, 480))
+
+        im.seek(1)
+        self.assertEqual(im.size, (680, 480))
+
     def test_mp(self):
         for test_file in test_files:
             im = Image.open(test_file)