Repository: python-pillow/Pillow

Issue:
np.array(img) should not hide errors for corrupted images
### What did you do?
I spent a lot of time debugging a code which loads png images from disk and converts them to numpy arrays to do some processing.

### What did you expect to happen?

If I take any png and corrupt it, say, by truncating,
```
$ dd if=good.png of=corrupted.png bs=1024 count=10
$ python
```
 I expect the following code to fail with some exception because image pixels can not be read into an array.
```pycon
>>> from PIL import Image
>>> import numpy as np
>>> img = Image.open('corrupted.png') 
>>> arr = np.array(img)
```

### What actually happened?
Instead, I got an object array with a single element:
```pycon
>>> print(arr)
<PIL.PngImagePlugin.PngImageFile image mode=RGB size=384x288 at 0x7F38258C6240>
```

and now the code fails later with a strange error message, like:
```pycon
>>> arr / 255
TypeError: unsupported operand type(s) for /: 'PngImageFile' and 'int'
```

#### img.verify() behaves strangely, too
Funny and counter-intuitive thing, if I try instead doing:  
```pycon
>>> img = Image.open('good.png') 
>>> img.verify()
>>> arr = np.array(img)
>>> arr / 255
TypeError: unsupported operand type(s) for /: 'PngImageFile' and 'int'
```
I still got an error presumably because calling verify() makes image unusable, but again, np.array() doesn't care. So now the code fails for all good images.

### What are your OS, Python and Pillow versions?

* OS: ubuntu-18.04
* Python: 3.6
* Pillow: 8.0.1



Discussion:
When I try and replicate this, I find that
```python
img.__array_interface__
```
throws an OSError, but
```python
np.array(img)
```
doesn't. So depending on how you look at this, the problem is either that Pillow's `__array_interface__` isn't throwing the right kind of exception, or it is that numpy is catching the OSError.
I think this shows up in other guises, like https://github.com/python-pillow/Pillow/issues/3863 

Masking the real error is not ideal indeed.
Confirmed to be a bug in numpy - see the discussion starting at https://gitter.im/numpy/numpy?at=6067b4ef3153ce63a3b6dcad

<img width="626" alt="Screen Shot 2021-04-03 at 11 44 27 am" src="https://user-images.githubusercontent.com/3112309/113463544-feae8e80-9471-11eb-8519-b907fb3823ea.png">

We could raise a `RecursionError` or `MemoryError` to make `np.array` fail, but that wouldn't be reflecting the cause of the error.

Resolving patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -676,9 +676,10 @@ def _repr_png_(self):
             raise ValueError("Could not save to PNG for display") from e
         return b.getvalue()
 
-    @property
-    def __array_interface__(self):
+    def __array__(self):
         # numpy array interface support
+        import numpy as np
+
         new = {}
         shape, typestr = _conv_type_shape(self)
         new["shape"] = shape
@@ -690,7 +691,11 @@ def __array_interface__(self):
             new["data"] = self.tobytes("raw", "L")
         else:
             new["data"] = self.tobytes()
-        return new
+
+        class ArrayData:
+            __array_interface__ = new
+
+        return np.array(ArrayData())
 
     def __getstate__(self):
         return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()]


Test patch:
diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py
--- a/Tests/test_image_array.py
+++ b/Tests/test_image_array.py
@@ -4,26 +4,32 @@
 
 from .helper import hopper
 
+numpy = pytest.importorskip("numpy", reason="NumPy not installed")
+
 im = hopper().resize((128, 100))
 
 
 def test_toarray():
     def test(mode):
-        ai = im.convert(mode).__array_interface__
-        return ai["version"], ai["shape"], ai["typestr"], len(ai["data"])
+        ai = numpy.array(im.convert(mode))
+        return ai.shape, ai.dtype.str, ai.nbytes
 
-    # assert test("1") == (3, (100, 128), '|b1', 1600))
-    assert test("L") == (3, (100, 128), "|u1", 12800)
+    # assert test("1") == ((100, 128), '|b1', 1600))
+    assert test("L") == ((100, 128), "|u1", 12800)
 
     # FIXME: wrong?
-    assert test("I") == (3, (100, 128), Image._ENDIAN + "i4", 51200)
+    assert test("I") == ((100, 128), Image._ENDIAN + "i4", 51200)
     # FIXME: wrong?
-    assert test("F") == (3, (100, 128), Image._ENDIAN + "f4", 51200)
+    assert test("F") == ((100, 128), Image._ENDIAN + "f4", 51200)
+
+    assert test("LA") == ((100, 128, 2), "|u1", 25600)
+    assert test("RGB") == ((100, 128, 3), "|u1", 38400)
+    assert test("RGBA") == ((100, 128, 4), "|u1", 51200)
+    assert test("RGBX") == ((100, 128, 4), "|u1", 51200)
 
-    assert test("LA") == (3, (100, 128, 2), "|u1", 25600)
-    assert test("RGB") == (3, (100, 128, 3), "|u1", 38400)
-    assert test("RGBA") == (3, (100, 128, 4), "|u1", 51200)
-    assert test("RGBX") == (3, (100, 128, 4), "|u1", 51200)
+    with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
+        with pytest.raises(OSError):
+            numpy.array(im_truncated)
 
 
 def test_fromarray():
@@ -39,10 +45,18 @@ def tobytes(self):
 
     def test(mode):
         i = im.convert(mode)
-        a = i.__array_interface__
-        a["strides"] = 1  # pretend it's non-contiguous
+        a = numpy.array(i)
         # Make wrapper instance for image, new array interface
-        wrapped = Wrapper(i, a)
+        wrapped = Wrapper(
+            i,
+            {
+                "shape": a.shape,
+                "typestr": a.dtype.str,
+                "version": 3,
+                "data": a.data,
+                "strides": 1,  # pretend it's non-contiguous
+            },
+        )
         out = Image.fromarray(wrapped)
         return out.mode, out.size, list(i.getdata()) == list(out.getdata())