Repository: python-pillow/Pillow

Issue:
Image.getcolors doesn't work as expected with P mode images
<!--
Thank you for reporting an issue.

Follow these guidelines to ensure your issue is handled properly.

If you have a ...

1.  General question: consider asking the question on Stack Overflow
    with the python-imaging-library tag:

    * https://stackoverflow.com/questions/tagged/python-imaging-library

    Do not ask a question in both places.

    If you think you have found a bug or have an unexplained exception
    then file a bug report here.

2.  Bug report: include a self-contained, copy-pastable example that
    generates the issue if possible. Be concise with code posted.
    Guidelines on how to provide a good bug report:

    * https://stackoverflow.com/help/mcve

    Bug reports which follow these guidelines are easier to diagnose,
    and are often handled much more quickly.

3.  Feature request: do a quick search of existing issues
    to make sure this has not been asked before.

We know asking good questions takes effort, and we appreciate your time.
Thank you.
-->

### What did you do?

I called `getcolors()` on a `P` mode image.

### What did you expect to happen?

I expected to get a list with as many tuples of the form `(count, (R, G, B))` as there are colors in the palette.

### What actually happened?

Got a list of tuples of the form `(count, color_index)` instead.

### What are your OS, Python and Pillow versions?

* OS: Windows 10
* Python: 3.9.1
* Pillow: 8.1.0

<!--
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.

The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as Plone, Django, or Buildout, try to replicate the issue just using Pillow.
-->

```python
>>> from PIL import Image
>>> im = Image.open('hopper_P.png')
>>> print(im.getcolors())
[(7397, 0), (2383, 1), (639, 2), (731, 3), (1857, 4), (800, 5), (1629, 6), (948, 7)]
```
I would expect:
```python
[(7397, (25, 20, 35)), (2383, (112, 143, 198)), (639, (144, 37, 34)), (731, (240, 229, 225)), (1857, (80, 108, 166)), (800, (152, 97, 78)), (1629, (214, 158, 130)), (948, (195, 113, 80))]
```

I think it could be fixed by adding the following block:
```python
            if self.mode == "P":
                palette = self.getpalette()
                palette = [palette[i:i + 3] for i in range(0, len(palette), 3)]  # divide palette list in chunks of length 3
                out = [(count, tuple(palette[i])) for (count, i) in out]
```
right before:
https://github.com/python-pillow/Pillow/blob/cfff80ad76cbe1f7021c96c1899ead8b20d369f1/src/PIL/Image.py#L1261-L1262

![hopper_P](https://user-images.githubusercontent.com/20274082/103940110-9f0f4400-510b-11eb-9e27-4915bc6d9e43.png)


Discussion:
Pillow values backwards compatibility. I don't think this is definitely an incorrect result, and some users might be relying on the current functionality.

We could allow your output by adding another optional argument to `getcolors()`, so that the default behaviour doesn't change - however, I don't know what new argument that would be... `getcolors(output="RGB")`?

Also, I don't think there's anything special about "P" in your line of thinking. The same logic would also apply to "1" and "L".

An alternative would be to just improve the documentation of the current situation.
I understand the part about backwards compatibility. An optional argument like the one you suggest would be fine by me.

The special thing about `1` and `L` modes is that the color index coincides with the color value (in grayscale).

I you think it's worth it I could submit a PR.
Rather than extending the API, could you instead convert your image to RGB?

My thinking is, the image is in P mode, so you're getting the colour counts of a P-mode image. As @radarhere says, there's also 1 and L mode images. And if we extend the API here, are there others that should also be extended in a similar way? How common a use case is this?
> Rather than extending the API, could you instead convert your image to RGB?

I think that's a reasonable compromise.

> My thinking is...

Not sure about how common it might be. I just ran into it a few weeks ago and found the behavior unexpected. Maybe the docstring could be improved to something like:
```
:returns: An unsorted list of (count, pixel) values. For ``1``, ``L`` and ``P`` image modes, ``pixel`` is the color index. For ``RGB`` mode ``pixel`` is a tuple of `(R, G, B)` values
```

Resolving patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -1243,6 +1243,10 @@ def getcolors(self, maxcolors=256):
         """
         Returns a list of colors used in this image.
 
+        The colors will be in the image's mode. For example, an RGB image will
+        return a tuple of (red, green, blue) color values, and a P image will
+        return the index of the color in the palette.
+
         :param maxcolors: Maximum number of colors.  If this number is
            exceeded, this method returns None.  The default limit is
            256 colors.


Test patch: