Repository: python-pillow/Pillow

Problem statement:
Corrupt jpeg opens as all black image
### What did you do?
```python
from PIL import Image
from PIL import ImageFile

ImageFile.LOAD_TRUNCATED_IMAGES = True

im = Image.open('pic263134.jpg')
im.save('1.png')
```
### What did you expect to happen?
Image could be saved up to corruption point. 
### What actually happened?
All black image is saved. Tried saving to different output formats - it's all black anyway.
![1.png](https://user-images.githubusercontent.com/2279051/36819127-dc9e33ea-1c9c-11e8-9a93-0d3c0a674f02.png)

### What versions of Pillow and Python are you using?
Python2. Pillow 5.0.0 and 3.0.0

Original image:
![original_image](https://user-images.githubusercontent.com/2279051/36819109-d04f6f32-1c9c-11e8-97cf-fc244d5aa5e3.jpg)

`djpeg` can output image correctly (up to corruption point)

Looks like it's problem with decoding step, since after opening, all image bands contain 0-only bytes:
```python
sum(im.split()[0].point(lambda x: 0 if x else 1).getdata())
# returns 3097440
```



Hints:

Patch:
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -202,44 +202,39 @@ def load(self):
             for decoder_name, extents, offset, args in self.tile:
                 decoder = Image._getdecoder(self.mode, decoder_name,
                                             args, self.decoderconfig)
-                seek(offset)
-                decoder.setimage(self.im, extents)
-                if decoder.pulls_fd:
-                    decoder.setfd(self.fp)
-                    status, err_code = decoder.decode(b"")
-                else:
-                    b = prefix
-                    while True:
-                        try:
-                            s = read(self.decodermaxblock)
-                        except (IndexError, struct.error):  # truncated png/gif
-                            if LOAD_TRUNCATED_IMAGES:
-                                break
-                            else:
-                                raise IOError("image file is truncated")
-
-                        if not s:  # truncated jpeg
-                            self.tile = []
-
-                            # JpegDecode needs to clean things up here either way
-                            # If we don't destroy the decompressor,
-                            # we have a memory leak.
-                            decoder.cleanup()
-
-                            if LOAD_TRUNCATED_IMAGES:
+                try:
+                    seek(offset)
+                    decoder.setimage(self.im, extents)
+                    if decoder.pulls_fd:
+                        decoder.setfd(self.fp)
+                        status, err_code = decoder.decode(b"")
+                    else:
+                        b = prefix
+                        while True:
+                            try:
+                                s = read(self.decodermaxblock)
+                            except (IndexError, struct.error):  # truncated png/gif
+                                if LOAD_TRUNCATED_IMAGES:
+                                    break
+                                else:
+                                    raise IOError("image file is truncated")
+
+                            if not s:  # truncated jpeg
+                                if LOAD_TRUNCATED_IMAGES:
+                                    break
+                                else:
+                                    self.tile = []
+                                    raise IOError("image file is truncated "
+                                                  "(%d bytes not processed)" % len(b))
+
+                            b = b + s
+                            n, err_code = decoder.decode(b)
+                            if n < 0:
                                 break
-                            else:
-                                raise IOError("image file is truncated "
-                                              "(%d bytes not processed)" % len(b))
-
-                        b = b + s
-                        n, err_code = decoder.decode(b)
-                        if n < 0:
-                            break
-                        b = b[n:]
-
-                # Need to cleanup here to prevent leaks in PyPy
-                decoder.cleanup()
+                            b = b[n:]
+                finally:
+                    # Need to cleanup here to prevent leaks
+                    decoder.cleanup()
 
         self.tile = []
         self.readonly = readonly
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -353,6 +353,21 @@ def _open(self):
             else:
                 raise SyntaxError("no marker found")
 
+    def load_read(self, read_bytes):
+        """
+        internal: read more image data
+        For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker
+        so libjpeg can finish decoding
+        """
+        s = self.fp.read(read_bytes)
+
+        if not s and ImageFile.LOAD_TRUNCATED_IMAGES:
+            # Premature EOF.
+            # Pretend file is finished adding EOI marker
+            return b"\xFF\xD9"
+
+        return s
+
     def draft(self, mode, size):
 
         if len(self.tile) != 1:


Test patch:
diff --git a/Tests/images/truncated_jpeg.jpg b/Tests/images/truncated_jpeg.jpg
new file mode 100644
Binary files /dev/null and b/Tests/images/truncated_jpeg.jpg differ
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -348,6 +348,22 @@ def test_ff00_jpeg_header(self):
         filename = "Tests/images/jpeg_ff00_header.jpg"
         Image.open(filename)
 
+    def test_truncated_jpeg_should_read_all_the_data(self):
+        filename = "Tests/images/truncated_jpeg.jpg"
+        ImageFile.LOAD_TRUNCATED_IMAGES = True
+        im = Image.open(filename)
+        im.load()
+        ImageFile.LOAD_TRUNCATED_IMAGES = False
+        self.assertIsNotNone(im.getbbox())
+
+    def test_truncated_jpeg_throws_IOError(self):
+        filename = "Tests/images/truncated_jpeg.jpg"
+        im = Image.open(filename)
+
+        with self.assertRaises(IOError):
+            im.load()
+
+
     def _n_qtables_helper(self, n, test_file):
         im = Image.open(test_file)
         f = self.tempfile('temp.jpg')