Repository: python-pillow/Pillow
Pull request: Allow tuples with one item to give single color value in getink
Resolves #4925

Normally, a user would run `Image.new('F', (100,100), 255)` and `Image.new('I', (100,100), 255)`, providing a single value for `color` for those modes.

This PR allows users to also provide that single value inside a tuple, `Image.new('F', (200,100), (255,))` and `Image.new('I', (200,100), (255,))`, which would have previously thrown an error.

Problem statement:
SystemError on Image.new / fill for 32-bit mode with color tuple
The following works smoothly:

```python
>>> PIL.Image.new('L', (200,100), (255,)) # accepts tuple
```

However, for 32-bit modes, this does not work:
```python
>>> PIL.Image.new('F', (200,100), (255,)) # fails okay
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lib/python3.6/site-packages/PIL/Image.py", line 2613, in new
    return im._new(core.fill(mode, size, color))
TypeError: must be real number, not tuple
>>> PIL.Image.new('I', (200,100), (255,)) # fails badly
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lib/python3.6/site-packages/PIL/Image.py", line 2613, in new
    return im._new(core.fill(mode, size, color))
SystemError: <built-in function fill> returned NULL without setting an error
```

At least the error message in the latter case should be improved.

But why not be more tolerant in both cases (I or F), like above (L or P)?

(Seen with Pillow 7.2.0 on Python 3.6.7 Linux x86_64.)

Hints from issue discussion:
Note: always allowing tuple would make it easier to pass values from `ImageStat.Stat().median` etc. (without checking for bands and unlisting)
With #4882 in master the last one has a proper error message:
```python
>>> PIL.Image.new('I', (200,100), (255,))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "c:\git\pillow\src\PIL\Image.py", line 2621, in new
    return im._new(core.fill(mode, size, color))
TypeError: color must be int
```

Commit message: Allow tuples with one item to give single color value in getink

Patch to review:
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 89894c9a773..ef0f1477f74 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -498,6 +498,12 @@ def test_storage_neg(self):
         with pytest.raises(ValueError):
             Image.core.fill("RGB", (2, -2), (0, 0, 0))
 
+    def test_one_item_tuple(self):
+        for mode in ("I", "F", "L"):
+            im = Image.new(mode, (100, 100), (5,))
+            px = im.load()
+            assert px[0, 0] == 5
+
     def test_linear_gradient_wrong_mode(self):
         # Arrange
         wrong_mode = "RGB"
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index 9afeb77f7ae..5ec3120463d 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -328,26 +328,17 @@ def test_p_putpixel_rgb_rgba(self):
 
 
 class TestImagePutPixelError(AccessTest):
-    IMAGE_MODES1 = ["L", "LA", "RGB", "RGBA"]
-    IMAGE_MODES2 = ["I", "I;16", "BGR;15"]
-    INVALID_TYPES1 = ["foo", 1.0, None]
-    INVALID_TYPES2 = [*INVALID_TYPES1, (10,)]
+    IMAGE_MODES = ["L", "LA", "RGB", "RGBA", "I", "I;16", "BGR;15"]
+    INVALID_TYPES = ["foo", 1.0, None]
 
-    @pytest.mark.parametrize("mode", IMAGE_MODES1)
-    def test_putpixel_type_error1(self, mode):
+    @pytest.mark.parametrize("mode", IMAGE_MODES)
+    def test_putpixel_type_error(self, mode):
         im = hopper(mode)
-        for v in self.INVALID_TYPES1:
+        for v in self.INVALID_TYPES:
             with pytest.raises(TypeError, match="color must be int or tuple"):
                 im.putpixel((0, 0), v)
 
-    @pytest.mark.parametrize("mode", IMAGE_MODES2)
-    def test_putpixel_type_error2(self, mode):
-        im = hopper(mode)
-        for v in self.INVALID_TYPES2:
-            with pytest.raises(TypeError, match="color must be int"):
-                im.putpixel((0, 0), v)
-
-    @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
+    @pytest.mark.parametrize("mode", IMAGE_MODES)
     def test_putpixel_overflow_error(self, mode):
         im = hopper(mode)
         with pytest.raises(OverflowError):
diff --git a/src/_imaging.c b/src/_imaging.c
index d3159a49415..58975900ad7 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -518,6 +518,9 @@ getink(PyObject* color, Imaging im, char* ink)
        be cast to either UINT8 or INT32 */
 
     int rIsInt = 0;
+    if (PyTuple_Check(color) && PyTuple_Size(color) == 1) {
+        color = PyTuple_GetItem(color, 0);
+    }
     if (im->type == IMAGING_TYPE_UINT8 ||
         im->type == IMAGING_TYPE_INT32 ||
         im->type == IMAGING_TYPE_SPECIAL) {
@@ -533,7 +536,7 @@ getink(PyObject* color, Imaging im, char* ink)
                 return NULL;
             }
         } else {
-            PyErr_SetString(PyExc_TypeError, "color must be int");
+            PyErr_SetString(PyExc_TypeError, "color must be int or tuple");
             return NULL;
         }
     }


Code review comments:
diff_hunk: @@ -533,7 +536,7 @@ getink(PyObject* color, Imaging im, char* ink)
                 return NULL;
             }
         } else {
-            PyErr_SetString(PyExc_TypeError, "color must be int");
+            PyErr_SetString(PyExc_TypeError, "color must be int or tuple");
path: src/_imaging.c
text: @user1:
```python
>>> PIL.Image.new('I', (200,100), (255,255,255))
...
TypeError: color must be int or tuple
```
is not very helpful.

The following suggestion would avoid confusion if a user specifies a tuple with two or more values:
```suggestion
            PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple");
```

The `IMAGING_TYPE_UINT8` branch provides a more specific error message for an invalid tuple below (from `PyArg_ParseTuple`).