Repository: python-pillow/Pillow

Issue:
Refactor _MODE_CONV into ImageModes
Currently, Image.py has a private variable called `_MODE_CONV` that looks like this:

https://github.com/python-pillow/Pillow/blob/8e4a618b550b1a67e31c5362552db74b2cc72841/src/PIL/Image.py#L211-L239

I find this constant quite useful, and I was wondering if this info can be made part of `ImageMode` and exposed publically, so that I can (confidently) use it in downstream code. I think it would also make this codebase cleaner overall :D


For my use case, I am pre-allocating buffers into which I later want to decode image data, i.e., I have something similar to [`_conv_type_shape`](https://github.com/python-pillow/Pillow/blob/8e4a618b550b1a67e31c5362552db74b2cc72841/src/PIL/Image.py#L242) and would like to keep it in sync with the modes supported by pillow (without having to update manually).


Discussion:
I realise this probably doesn't resolve your request, but here is a way to get that information using public APIs.

```python
from PIL import Image
import numpy

for mode in ("1", "L", "LA", "I", "F", "P", "RGB", "RGBX", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I;16"):
    typ_extra = Image._MODE_CONV[mode]

    a = numpy.array(Image.new(mode, (0, 0)))
    typ_extra_from_public_api = (a.dtype.str, None if a.ndim < 3 else a.shape[2])

    assert typ_extra_from_public_api == typ_extra
```
That's a very cool solution to the problem indeed. It solves my use-case, so this would be a workable solution from my side. 

That said, I do wonder though if the suggested refactor would still be worthwhile.
@radarhere I just tested the snipped you shared, and it fails for me when calling `np.array` on the image. I tested with pillow 8.4,0, pillow9, numpy 1.21.4 and numpy 1.22.1. Could you let me know which versions you are using when running this snippet?

Edit: This is the traceback

```python
Traceback (most recent call last):
  File ".\bar.py", line 7, in <module>
    a = numpy.array(Image.new(mode, (0, 0)))
  File "C:\Users\Sebastian\AppData\Local\Programs\Python\Python38\lib\site-packages\PIL\Image.py", line 673, in __array__
    new["data"] = self.tobytes("raw", "L")
  File "C:\Users\Sebastian\AppData\Local\Programs\Python\Python38\lib\site-packages\PIL\Image.py", line 722, in tobytes
    e.setimage(self.im)
SystemError: tile cannot extend outside image
```

Edit 2: 

I found the culprit. It should be `a = numpy.array(Image.new(mode, (1, 1)))`.
Oh, my mistake, it would have been because I was using main.
To use Pillow 9.0, just change the size of the image from (0, 0) to (1, 1).
Thinking about this, rather than making `_MODE_CONV` public, it might be better to make `_conv_type_shape` public, a method rather than a dictionary, so that we have more control. That could be useful in the future if we ever need to deprecate anything.

`_conv_type_shape` takes an image as the input, rather than a mode. That seems right, because I think the approach of our API is generally about dealing with images, not modes.

But once I talk about a public method that returns the shape and type of a NumPy array from an image, I start to think it sounds the same as `__array__` and the code I shared earlier -
```python
a = numpy.array(Image.new(mode, (0, 0)))
typ_extra_from_public_api = (a.dtype.str, None if a.ndim < 3 else a.shape[2])
```

So I don't think I'm convinced about this one.

Resolving patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -203,48 +203,16 @@ def isImageType(t):
 # --------------------------------------------------------------------
 # Modes
 
-if sys.byteorder == "little":
-    _ENDIAN = "<"
-else:
-    _ENDIAN = ">"
-
-_MODE_CONV = {
-    # official modes
-    "1": ("|b1", None),  # Bits need to be extended to bytes
-    "L": ("|u1", None),
-    "LA": ("|u1", 2),
-    "I": (_ENDIAN + "i4", None),
-    "F": (_ENDIAN + "f4", None),
-    "P": ("|u1", None),
-    "RGB": ("|u1", 3),
-    "RGBX": ("|u1", 4),
-    "RGBA": ("|u1", 4),
-    "CMYK": ("|u1", 4),
-    "YCbCr": ("|u1", 3),
-    "LAB": ("|u1", 3),  # UNDONE - unsigned |u1i1i1
-    "HSV": ("|u1", 3),
-    # I;16 == I;16L, and I;32 == I;32L
-    "I;16": ("<u2", None),
-    "I;16B": (">u2", None),
-    "I;16L": ("<u2", None),
-    "I;16S": ("<i2", None),
-    "I;16BS": (">i2", None),
-    "I;16LS": ("<i2", None),
-    "I;32": ("<u4", None),
-    "I;32B": (">u4", None),
-    "I;32L": ("<u4", None),
-    "I;32S": ("<i4", None),
-    "I;32BS": (">i4", None),
-    "I;32LS": ("<i4", None),
-}
+_ENDIAN = "<" if sys.byteorder == "little" else ">"
 
 
 def _conv_type_shape(im):
-    typ, extra = _MODE_CONV[im.mode]
-    if extra is None:
-        return (im.size[1], im.size[0]), typ
-    else:
-        return (im.size[1], im.size[0], extra), typ
+    m = ImageMode.getmode(im.mode)
+    shape = (im.height, im.width)
+    extra = len(m.bands)
+    if extra != 1:
+        shape += (extra,)
+    return shape, m.typestr
 
 
 MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"]
diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py
--- a/src/PIL/ImageMode.py
+++ b/src/PIL/ImageMode.py
@@ -13,6 +13,8 @@
 # See the README file for information on usage and redistribution.
 #
 
+import sys
+
 # mode descriptor cache
 _modes = None
 
@@ -20,11 +22,12 @@
 class ModeDescriptor:
     """Wrapper for mode strings."""
 
-    def __init__(self, mode, bands, basemode, basetype):
+    def __init__(self, mode, bands, basemode, basetype, typestr):
         self.mode = mode
         self.bands = bands
         self.basemode = basemode
         self.basetype = basetype
+        self.typestr = typestr
 
     def __str__(self):
         return self.mode
@@ -36,43 +39,53 @@ def getmode(mode):
     if not _modes:
         # initialize mode cache
         modes = {}
-        for m, (basemode, basetype, bands) in {
+        endian = "<" if sys.byteorder == "little" else ">"
+        for m, (basemode, basetype, bands, typestr) in {
             # core modes
-            "1": ("L", "L", ("1",)),
-            "L": ("L", "L", ("L",)),
-            "I": ("L", "I", ("I",)),
-            "F": ("L", "F", ("F",)),
-            "P": ("P", "L", ("P",)),
-            "RGB": ("RGB", "L", ("R", "G", "B")),
-            "RGBX": ("RGB", "L", ("R", "G", "B", "X")),
-            "RGBA": ("RGB", "L", ("R", "G", "B", "A")),
-            "CMYK": ("RGB", "L", ("C", "M", "Y", "K")),
-            "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")),
-            "LAB": ("RGB", "L", ("L", "A", "B")),
-            "HSV": ("RGB", "L", ("H", "S", "V")),
+            # Bits need to be extended to bytes
+            "1": ("L", "L", ("1",), "|b1"),
+            "L": ("L", "L", ("L",), "|u1"),
+            "I": ("L", "I", ("I",), endian + "i4"),
+            "F": ("L", "F", ("F",), endian + "f4"),
+            "P": ("P", "L", ("P",), "|u1"),
+            "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"),
+            "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"),
+            "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"),
+            "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"),
+            "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"),
+            # UNDONE - unsigned |u1i1i1
+            "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"),
+            "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"),
             # extra experimental modes
-            "RGBa": ("RGB", "L", ("R", "G", "B", "a")),
-            "BGR;15": ("RGB", "L", ("B", "G", "R")),
-            "BGR;16": ("RGB", "L", ("B", "G", "R")),
-            "BGR;24": ("RGB", "L", ("B", "G", "R")),
-            "BGR;32": ("RGB", "L", ("B", "G", "R")),
-            "LA": ("L", "L", ("L", "A")),
-            "La": ("L", "L", ("L", "a")),
-            "PA": ("RGB", "L", ("P", "A")),
+            "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"),
+            "BGR;15": ("RGB", "L", ("B", "G", "R"), endian + "u2"),
+            "BGR;16": ("RGB", "L", ("B", "G", "R"), endian + "u2"),
+            "BGR;24": ("RGB", "L", ("B", "G", "R"), endian + "u3"),
+            "BGR;32": ("RGB", "L", ("B", "G", "R"), endian + "u4"),
+            "LA": ("L", "L", ("L", "A"), "|u1"),
+            "La": ("L", "L", ("L", "a"), "|u1"),
+            "PA": ("RGB", "L", ("P", "A"), "|u1"),
         }.items():
-            modes[m] = ModeDescriptor(m, bands, basemode, basetype)
+            modes[m] = ModeDescriptor(m, bands, basemode, basetype, typestr)
         # mapping modes
-        for i16mode in (
-            "I;16",
-            "I;16S",
-            "I;16L",
-            "I;16LS",
-            "I;16B",
-            "I;16BS",
-            "I;16N",
-            "I;16NS",
-        ):
-            modes[i16mode] = ModeDescriptor(i16mode, ("I",), "L", "L")
+        for i16mode, typestr in {
+            # I;16 == I;16L, and I;32 == I;32L
+            "I;16": "<u2",
+            "I;16S": "<i2",
+            "I;16L": "<u2",
+            "I;16LS": "<i2",
+            "I;16B": ">u2",
+            "I;16BS": ">i2",
+            "I;16N": endian + "u2",
+            "I;16NS": endian + "i2",
+            "I;32": "<u4",
+            "I;32B": ">u4",
+            "I;32L": "<u4",
+            "I;32S": "<i4",
+            "I;32BS": ">i4",
+            "I;32LS": "<i4",
+        }.items():
+            modes[i16mode] = ModeDescriptor(i16mode, ("I",), "L", "L", typestr)
         # set global mode cache atomically
         _modes = modes
     return _modes[mode]


Test patch:
diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py
--- a/Tests/test_image_mode.py
+++ b/Tests/test_image_mode.py
@@ -21,6 +21,7 @@ def test_sanity():
     assert m.bands == ("1",)
     assert m.basemode == "L"
     assert m.basetype == "L"
+    assert m.typestr == "|b1"
 
     for mode in (
         "I;16",
@@ -45,6 +46,7 @@ def test_sanity():
     assert m.bands == ("R", "G", "B")
     assert m.basemode == "RGB"
     assert m.basetype == "L"
+    assert m.typestr == "|u1"
 
 
 def test_properties():