Repository: python-pillow/Pillow

Issue:
Incorrect SampleFormat and BitsPerSample
Images with signed integer data are all incorrectly assumed to be `int32`. `uint32` and `float64` are also incorrectly determined.
```python
import numpy as np
from PIL import Image
from tempfile import TemporaryFile

for dt in [np.bool,
           np.uint8, np.uint16, np.uint32,
           np.int8,  np.int16,  np.int32,
           np.float32, np.float64]:
    with TemporaryFile() as fp:
        before = np.array([[0, 1], [0, 1]], dtype = dt)
        img = Image.fromarray(before)
        img.save(fp, format = "TIFF")
        img = Image.open(fp)
        after = np.asarray(img)
        print("{0: >7} -> {1: >7}".format(str(before.dtype), str(after.dtype)))
```
gives
```
   bool ->    bool
  uint8 ->   uint8
 uint16 ->  uint16
 uint32 ->   int32
   int8 ->   int32
  int16 ->   int32
  int32 ->   int32
float32 -> float32
float64 -> float32
```


Discussion:
In Pillow, we translate input data into a few standard modes. So, for example, we don't have a mode for float64. Instead, we unpack a variety of floating point types into just "F" - https://github.com/python-pillow/Pillow/blob/0bd97c3df91f998d9644f524b10d612f2a4c759d/src/libImaging/Unpack.c#L1700-L1721

See also https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes.

It would be rather big change to abandon this idea in order to ensure that all of NumPy's modes could be roundtripped through Pillow.
I guess the only potential "bug" here is the uint32 -> int32 conversion, there should be no reason not to preserve this?
@kmilos are you saying that all "I" modes should go back to NumPy as uint32? So in the context of this issue, that would also mean int8, int16 and int32 would also become uint32.
> are you saying that all "I" modes should go back to NumPy as uint32?

No, just that there is a potential data loss/(or wraparound) in just uint32->int32 specifically, i.e. Pillow might be missing something like an "U" mode or "I;32U" modifier, or another way to indicate to Numpy how to cast internal int32 back.
The "I" mode has a range of int32, so it makes sense to me for that to become int32 when converted to NumPy.

You specifically called out uint32 -> int32, but I think the same argument would apply to float64 -> float32. There might indeed be data loss - but in the conversion from NumPy to Pillow in the first place, because the original NumPy types have a higher maximum than the Pillow modes.

Pillow mostly doesn't retain the information about the rawmode that an image had. Once the information is loaded, it might be transformed. A user might decide to scale the values down, given that int32 has a lower maximum value than uint32. If the user later converted that back to NumPy, they might be surprised that Pillow had remembered the original format of the data. I don't think we should obscure the fact that the range of the values decreased in this process.

Also, just to be clear - this issue has nothing to do with the TIFF format, but just concerns the interface between NumPy and Pillow.
> You specifically called out uint32 -> int32, but I think the same argument would apply to float64 -> float32.

It's not 100% the same - the former has to deal w/ sign and wraparound, the latter doesn't (only precision and range).

In any case, yes, the NumPy <-> Pillow interface should be transparently documented with all of its quirks and gotchas.

Resolving patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -3031,21 +3031,29 @@ def frombuffer(mode, size, data, decoder_name="raw", *args):
 def fromarray(obj, mode=None):
     """
     Creates an image memory from an object exporting the array interface
-    (using the buffer protocol).
+    (using the buffer protocol)::
+
+      from PIL import Image
+      import numpy as np
+      a = np.zeros((5, 5))
+      im = Image.fromarray(a)
 
     If ``obj`` is not contiguous, then the ``tobytes`` method is called
     and :py:func:`~PIL.Image.frombuffer` is used.
 
-    If you have an image in NumPy::
+    In the case of NumPy, be aware that Pillow modes do not always correspond
+    to NumPy dtypes. Pillow modes only offer 1-bit pixels, 8-bit pixels,
+    32-signed integer pixels and 32-bit floating point pixels.
+
+    Pillow images can also be converted to arrays::
 
       from PIL import Image
       import numpy as np
       im = Image.open("hopper.jpg")
       a = np.asarray(im)
 
-    Then this can be used to convert it to a Pillow image::
-
-      im = Image.fromarray(a)
+    When converting Pillow images to arrays however, only pixel values are
+    transferred. This means that P and PA mode images will lose their palette.
 
     :param obj: Object with array interface
     :param mode: Optional mode to use when reading ``obj``. Will be determined from


Test patch: