Repository: python-pillow/Pillow

Issue:
Image.putdata() NumPy usability suggestion
This could be a feature request, an ask for a doc change or for better error reporting.

When I grab an image, suck it into a numpy array, process it, and then spit it back into an image, I hit what appears to be a silent fail when the data given to Image.putdata() is a 2D numpy array instead of 1D (nominally a sequence), or is a float array instead of uint8.

My naive expectation was to do this:

```python
image_array = np.array(source_image)
# ...do something to the data via numpy that might inadvertently 
# change it to float64, still with int-equivalent values...
new_image = Image.new('P', (source_image.size))
new_image.putdata(image_array)
```
If I don't coerce the array to uint8 and 1D, I still get an image, but with all values set to 0, and Pillow is silent on what (again, naively) appears to be a failure.

(I now know `new_image.putdata(image_array.ravel().astype(int))` works, but it wan't obvious. :)  

While making the docs more precise or adding better error reporting are both easier responses, it would seem that this isn't an uncommon use case, and it might be a nice ease-of-use change to have putdata accept a 2D array if it's the same size as the image, and also to coerce the data to ints, as a better silent option. 

I'm running Pillow 8.4.0, python 3.10 on osx Mojave 10.14.6.

Thanks!

***

Test code:

```python
from PIL import Image
import numpy as np


palette_data = [
		0, 0, 192,
		0, 33, 76,
		0, 192, 0,
		0, 192, 192,
		9, 9, 9,
		19, 19, 19,
		29, 29, 29,
		50, 0, 106,
		192, 0, 0,
		192, 0, 192,
		192, 192, 0,
		192, 192, 192,
		255, 255, 255
		]

image_data = np.array([
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [11., 11., 11., 11., 10., 10., 10., 10., 3., 3., 3., 3., 2., 2., 2., 2., 9., 9., 9., 9., 8., 8., 8., 8., 0., 0., 0., 0.],
                      [ 0., 0., 0., 0., 5., 5., 5., 5., 9., 9., 9., 9., 5., 5., 5., 5., 3., 3., 3., 3., 5., 5., 5., 5., 11., 11., 11., 11.],
                      [ 0., 0., 0., 0., 5., 5., 5., 5., 9., 9., 9., 9., 5., 5., 5., 5., 3., 3., 3., 3., 5., 5., 5., 5., 11., 11., 11., 11.],
                      [ 0., 0., 0., 0., 5., 5., 5., 5., 9., 9., 9., 9., 5., 5., 5., 5., 3., 3., 3., 3., 5., 5., 5., 5., 11., 11., 11., 11.],
                      [ 1., 1., 1., 1., 1., 12., 12., 12., 12., 12., 7., 7., 7., 7., 7., 5., 5., 5., 5., 5., 4., 5., 5., 6., 5., 5., 5., 5.],
                      [ 1., 1., 1., 1., 1., 12., 12., 12., 12., 12., 7., 7., 7., 7., 7., 5., 5., 5., 5., 5., 4., 5., 5., 6., 5., 5., 5., 5.],
                      [ 1., 1., 1., 1., 1., 12., 12., 12., 12., 12., 7., 7., 7., 7., 7., 5., 5., 5., 5., 5., 4., 5., 5., 6., 5., 5., 5., 5.],
                      [ 1., 1., 1., 1., 1., 12., 12., 12., 12., 12., 7., 7., 7., 7., 7., 5., 5., 5., 5., 5., 4., 5., 5., 6., 5., 5., 5., 5.],
                      [ 1., 1., 1., 1., 1., 12., 12., 12., 12., 12., 7., 7., 7., 7., 7., 5., 5., 5., 5., 5., 4., 5., 5., 6., 5., 5., 5., 5.],
                      [ 1., 1., 1., 1., 1., 12., 12., 12., 12., 12., 7., 7., 7., 7., 7., 5., 5., 5., 5., 5., 4., 5., 5., 6., 5., 5., 5., 5.],
                      [ 1., 1., 1., 1., 1., 12., 12., 12., 12., 12., 7., 7., 7., 7., 7., 5., 5., 5., 5., 5., 4., 5., 5., 6., 5., 5., 5., 5.]
                      ], dtype=np.float64)

new_image = Image.new('P', (28, 28), color=None)
new_image.putpalette(palette_data)

new_image.putdata(image_data.astype(int))
# Resulting image data is all zeroes.
new_image.show()
new_image.save('int_data_array.png')

new_image.putdata(image_data.ravel())
# Resulting image data is all zeroes.
new_image.show()
new_image.save('float_data_sequence.png')

new_image.putdata(image_data.ravel().astype(int))
# Resulting image data is correct, as expected.
new_image.show()
new_image.save('int_data_sequence.png')
```  

Correct image:

![int_data_sequence](https://user-images.githubusercontent.com/2294569/143942983-65d96ca2-ae5e-4f49-af93-b388e0447d8d.png)

Bad image cases:

![int_data_array](https://user-images.githubusercontent.com/2294569/143943146-86b1cf0d-de7c-4802-8164-6586548e50bb.png)

![float_data_sequence](https://user-images.githubusercontent.com/2294569/143943072-046271be-f333-4793-8069-e21ec5e54a85.png)


Discussion:
This isn't your main point - but when I run your code on my machine, float_data_sequence.png appears correctly.
Interesting. The attached files are literally the result of running the test here. I'm running numpy 1.21.4.

I took  a chance on stuffing data back into the same image for each variation. 

Separating them out make any difference? 

```python
new_image_1 = Image.new('P', (28, 28), color=None)
new_image_2 = Image.new('P', (28, 28), color=None)
new_image_3 = Image.new('P', (28, 28), color=None)
new_image_1.putpalette(palette_data)
new_image_2.putpalette(palette_data)
new_image_3.putpalette(palette_data)

# Resulting image data is all zeroes.
new_image_1.putdata(image_data.astype(int))
new_image_1.show()
new_image_1.save('int_data_array.png')

# Resulting image data is all zeroes.
new_image_2.putdata(image_data.ravel())
new_image_2.show()
new_image_2.save('float_data_sequence.png')

# Resulting image data is correct, as expected.
new_image_3.putdata(image_data.ravel().astype(int))
new_image_3.show()
new_image_3.save('int_data_sequence.png')
```

Resolving patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -1705,13 +1705,14 @@ def putalpha(self, alpha):
 
     def putdata(self, data, scale=1.0, offset=0.0):
         """
-        Copies pixel data to this image.  This method copies data from a
-        sequence object into the image, starting at the upper left
-        corner (0, 0), and continuing until either the image or the
-        sequence ends.  The scale and offset values are used to adjust
-        the sequence values: **pixel = value*scale + offset**.
-
-        :param data: A sequence object.
+        Copies pixel data from a flattened sequence object into the image. The
+        values should start at the upper left corner (0, 0), continue to the
+        end of the line, followed directly by the first value of the second
+        line, and so on. Data will be read until either the image or the
+        sequence ends. The scale and offset values are used to adjust the
+        sequence values: **pixel = value*scale + offset**.
+
+        :param data: A flattened sequence object.
         :param scale: An optional scale value.  The default is 1.0.
         :param offset: An optional offset value.  The default is 0.0.
         """


Test patch:
diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py
--- a/Tests/test_image_putdata.py
+++ b/Tests/test_image_putdata.py
@@ -1,6 +1,8 @@
 import sys
 from array import array
 
+import pytest
+
 from PIL import Image
 
 from .helper import assert_image_equal, hopper
@@ -47,6 +49,12 @@ def test_pypy_performance():
     im.putdata(list(range(256)) * 256)
 
 
+def test_mode_with_L_with_float():
+    im = Image.new("L", (1, 1), 0)
+    im.putdata([2.0])
+    assert im.getpixel((0, 0)) == 2
+
+
 def test_mode_i():
     src = hopper("L")
     data = list(src.getdata())
@@ -87,3 +95,18 @@ def test_array_F():
     im.putdata(arr)
 
     assert len(im.getdata()) == len(arr)
+
+
+def test_not_flattened():
+    im = Image.new("L", (1, 1))
+    with pytest.raises(TypeError):
+        im.putdata([[0]])
+    with pytest.raises(TypeError):
+        im.putdata([[0]], 2)
+
+    with pytest.raises(TypeError):
+        im = Image.new("I", (1, 1))
+        im.putdata([[0]])
+    with pytest.raises(TypeError):
+        im = Image.new("F", (1, 1))
+        im.putdata([[0]])