Repository: python-pillow/Pillow

Issue:
Saving grayscale 16 bit PNG fails
### What did you do?
Trying to save a grayscale 16 bit PNG.

### What did you expect to happen?
Obtain a 16 bit PNG file.

### What actually happened?
An error.

### What versions of Pillow and Python are you using?
Pillow 5.00
Python 3.5.4

Code:
```python
import numpy as np
from PIL import Image

ar = np.ones((32, 32), dtype=np.uint16)
im = Image.fromarray(ar)
im.save('foo.png')
```
Output:

> OSError: cannot write mode I;16 as PNG



Discussion:
There's a mapping from `I` mode, but not `I;16`. The current full list is here:
https://github.com/python-pillow/Pillow/blob/7bf5246b93cc89cfb9d6cca78c4719a943b10585/src/PIL/PngImagePlugin.py#L693-L708

It may be possible to add in a set of `I;16` mappings. 
Nice to hear. Also, casting to `I` could be a workaround until then. Thanks.
@tazr This seems to work. Is that what you mean by casting to `I`?

```py
import numpy as np
from PIL import Image

ar = np.ones((32, 32) , dtype=np.uint16)
ar32 = ar.astype(np.uint32)
im = Image.fromarray(ar32) # or more verbose as Image.fromarray(ar32, 'I')
im.save('foo.png')
```

@aclark4life So when/if the enhancement is made tarz's code shall be supported?

```py
import numpy as np
from PIL import Image

ar = np.ones((32, 32), dtype=np.uint16)
im = Image.fromarray(ar) # or more verbose as Image.fromarray(ar, 'I;16')
im.save('foo.png')
```
Is there any update on this? I tried with the latest pillow version and got an error
Same issue here. Curiously, the workaround for me involves using `imageio` (which in turn uses Pillow as the backend for png). I haven't yet had time to check _why_ this works, but I include it here in case someone encounters this in the future.

### Working Example
```python
import numpy as np
import imageio

arr = np.full((32, 32), 256, dtype=np.uint16)
imageio.imwrite("~/16-bit_test.png", arr)

img = imageio.imread("~/16-bit_test.png")
print(img.dtype)  # => "uint16"
```
and yields:
```
$ file ~/16-bit_test.png
16-bit_test.png: PNG image data, 32 x 32, 16-bit grayscale, non-interlaced
```
### Apparent Bug
When preparing the example above I noticed that using e.g. `np.ones` seems to produce an 8-bit output image. And indeed it appears that if the range of values in the source array will fit within uint8 the library takes the liberty to drop the top byte:
```python
import numpy as np
import imageio

arr = np.full((32, 32), 255, dtype=np.uint16)
imageio.imwrite("~/16-bit_test.png", arr)

img = imageio.imread("~/16-bit_test.png")
print(img.dtype)  # => "uint8"
```
and:
```
$ file ~/16-bit_test.png
16-bit_test.png: PNG image data, 32 x 32, 8-bit grayscale, non-interlaced
```
This is dangerous as e.g. low saturation input images will be falsely represented. I'll do some digging and raise an issue over at the `imageio` project so that this can hopefully be fixed.
Futher to my comment above I have extracted the approach that `imageio` takes which produces a working example using only Pillow:
```python
import numpy as np
from PIL import Image

arr = np.random.randint(0, 2**16-1, (32, 64), dtype=np.uint16)  # or np.ones etc.

array_buffer = arr.tobytes()
img = Image.new("I", arr.T.shape)
img.frombytes(array_buffer, 'raw', "I;16")
img.save("16-bit_test_pillow.png")
```
resulting in:
```
$ file 16-bit_test_pillow.png 
16-bit_test_pillow.png: PNG image data, 64 x 32, 16-bit grayscale, non-interlaced
```

Pillow: 5.1.0
Python: 3.6.5

I have created PR #3566 to address this.

Resolving patch:
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -707,6 +707,7 @@ def _getexif(self):
     "L":    ("L",       b'\x08\x00'),
     "LA":   ("LA",      b'\x08\x04'),
     "I":    ("I;16B",   b'\x10\x00'),
+    "I;16": ("I;16B",   b'\x10\x00'),
     "P;1":  ("P;1",     b'\x01\x03'),
     "P;2":  ("P;2",     b'\x02\x03'),
     "P;4":  ("P;4",     b'\x04\x03'),


Test patch:
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -88,20 +88,13 @@ def test_sanity(self):
         self.assertEqual(im.format, "PNG")
         self.assertEqual(im.get_format_mimetype(), 'image/png')
 
-        hopper("1").save(test_file)
-        Image.open(test_file)
-
-        hopper("L").save(test_file)
-        Image.open(test_file)
-
-        hopper("P").save(test_file)
-        Image.open(test_file)
-
-        hopper("RGB").save(test_file)
-        Image.open(test_file)
-
-        hopper("I").save(test_file)
-        Image.open(test_file)
+        for mode in ["1", "L", "P", "RGB", "I", "I;16"]:
+            im = hopper(mode)
+            im.save(test_file)
+            reloaded = Image.open(test_file)
+            if mode == "I;16":
+                reloaded = reloaded.convert(mode)
+            self.assert_image_equal(reloaded, im)
 
     def test_invalid_file(self):
         invalid_file = "Tests/images/flower.jpg"