Repository: python-pillow/Pillow

Problem statement:
Method to test if image has transparency/alpha
[**2023/09/11 edit**] adding a tl;dr:
1. Online sources do not agree on a single method to check for either transparency (actual transparent pixels) or the presence of an alpha channel (whether or not it is opaque), and for all the examples I've found, it's easy to craft examples on which they fail. It would be useful to have an authoritative correct solution from someone who really understands Pillow/images that handles all the edge cases. Or in lieu of that, we could crowd-source one here.
2. Given the frequency of the question and the widespread incorrect code, it seems like it'd be useful to add this feature to Pillow itself.

Original post:

I found myself wanting to check if an image had an alpha channel/transparency (I'm allowing for a distinction between those, perhaps there isn't a meaningful one). I was trying to convert some optimised images that didn't contain any transparency, so wanted to do something different in that case.

Checking for this is slightly tricky though, and I've seen incorrect logic posted elsewhere, e.g. [this stackoverflow post](https://stackoverflow.com/a/35859141/5506429) references [_this_ stackoverflow post](http://stackoverflow.com/a/1963146) which states to use:
```python
img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info)
```

But palette-mode images can store their transparency in at least two ways, in `img.info`, or the palette itself might be `RGBA`. If you run the following
```python
img = Image.new("RGBA", (100, 100), (128, 128, 128, 128)).quantize()
print(img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info)) # False
```

the result is `False`, but clearly it has alpha values in the palette:
```python
print(img.palette.mode) # 'RGBA'
print(img.palette.colors) # {(128, 128, 128, 128): 0, (0, 0, 0, 0): 1}
```

Would it be possible to add something like `img.has_alpha()` or `img.has_transparency()` to help here?

I'm not familiar enough with Pillow's image formats to say whether checking the mode of the palette in addition to the checks above would be exhaustive. Perhaps someone can clarify? Is it sufficient to check `img.palette.mode == 'RGBA'`? `.quantize()` does not seem to like an `LA` image, but if I manually set the palette mode and colors, it works with `show()`. Then there are other esoteric modes with alpha channels. So perhaps something like this will work?
```python
alpha_modes = {"RGBA", "RGBa", "LA", "La", "PA"}
return img.mode in alpha_modes or (
    img.mode == "P"
    and ("transparency" in img.info or img.palette.mode in alpha_modes)
)
```
but I very easily could have missed something due to inexperience with the unusual image formats.

(Happy to have a go at the implementation if that would be of assistance!)


Hints:
A side comment that I'm not sure how to phrase - you mention two StackOverflow posts, a 2016 StackOverflow post referencing a StackOverflow post from 2009. If the problem is that old, I have to imagine the awkward solutions are also well-ingrained in the behaviour of users by now, and that whatever happens here, posts from 2009 (which is actually before Pillow even existed) will still be there, offering a different way of doing things. This is just part of dealing with a library that has been around for a long time.

Given that you can have RGBA images that are fully solid, the method you're describing is sort of not describing whether an image **has** transparency, but whether it **might** have transparency. I presume you're not interested in a method that checks each pixel to see if it has transparency or not, because you correctly think that would be slower?
Thanks for the comment! Yeah I agree about the legacy here. But it's a bit of "the best time to act was in 2009, the second best time to act is now" sort of situation IMO. My point was that those well-ingrained solutions are actually _incorrect_, hence the huge benefit in a library-approved correct method (as opposed to just leaving people to use a commonly accepted workaround, which would be more okay if it was correct). I found those Stack Overflow posts through searching for sensible terms, and it's not uncommon to see a post on SO get a new solution many years later when a library gets updated, and that solution slowly gets upvoted.

Regarding solid RGBA images – that's why I left the door open for a semantic difference between "has alpha" and "has transparency". When it comes to my own use case, I was interested in not _just_ removing transparency but removing the alpha channel (even if it was opaque). Though I can see utility in both features, and while checking for transparency would be slower, I'm not sure if it would be too slow to be useful. I figured it was something that could come out in a discussion, anyway.

For clarity on terms: we're agreed that alpha does not imply transparency, does transparency imply alpha? Are there any ways to achieve transparency in Pillow that do not use an alpha channel? Genuine question as to whether this is a subset or a Venn diagram...!
A colleague just sent me this post, which seems like a nicely well-researched `has_transparency` method (unlike the one in my post which was not well-researched, and just supposed to be `has_alpha`)

[https://stackoverflow.com/a/58567453/5506429](https://stackoverflow.com/a/58567453/5506429)

though an answer by another author seems to check for different cases again

[https://stackoverflow.com/a/76268100/5506429](https://stackoverflow.com/a/76268100/5506429)
Though both of those answers rely on `transparency` being a key in `img.info`, which is not the case when you quantize an `RGBA` image

```python
img = Image.new("RGBA", (100, 100), (128, 128, 128, 128)).quantize()
print(img.info) # {}
```

perhaps that's actually a bug in `.quantize()` or `.new()` but seems all of these have some issues.
> Though both of those answers rely on `transparency` being a key in `img.info`, which is not the case when you quantize an `RGBA` image
> 
> ```python
> img = Image.new("RGBA", (100, 100), (128, 128, 128, 128)).quantize()
> print(img.info) # {}
> ```
> 
> perhaps that's actually a bug in `.quantize()` or `.new()` but seems all of these have some issues.

If the behaviour of `quantize()` is not to keep any transparency, then we probably don't want to change that default behaviour and defy existing user's expectations. Perhaps another argument could be added to enable transparency to stay around, but this seems to be getting away from your original request.

> Regarding solid RGBA images – that's why I left the door open for a semantic difference between "has alpha" and "has transparency".

Speaking for myself, I wouldn't have naturally understood a difference between those two method names.

> Are there any ways to achieve transparency in Pillow that do not use an alpha channel?

I think you've already posted an example of this, that P mode images can have a transparency index?

> When it comes to my own use case, I was interested in not just removing transparency but removing the alpha channel (even if it was opaque)

So this is the same as https://stackoverflow.com/questions/35859140/remove-transparency-alpha-from-any-image-using-pil/35859141#35859141. Would it be simpler to cut out the middleman of checking for transparency, and just have a method that removes transparency?
> If the behaviour of `quantize()` is not to keep any transparency, then we probably don't want to change that default behaviour and defy existing user's expectations. 

Sorry, `quantize()` _does_ retain transparency. That code sample I posted puts the alpha values into the palette. If you save it as a png and reload it, then it comes back in `.info['transparency']`.

> > Are there any ways to achieve transparency in Pillow that do not use an alpha channel?
> 
> I think you've already posted an example of this, that P mode images can have a transparency index?

Good point :)

> So this is the same as https://stackoverflow.com/questions/35859140/remove-transparency-alpha-from-any-image-using-pil/35859141#35859141. Would it be simpler to cut out the middleman of checking for transparency, and just have a method that removes transparency?

It does seem that removing transparency is the most common request here and I'd definitely make use of such a method, but I think it'd be complicated.

I specifically wanted to do something like:
```python
if has_alpha:
    composite with a specific colour background
    remove_alpha
```

but in that case, after `alpha_composite` all the alpha values are `255` so for `remove_alpha` I was happy to do `.convert("RGB")`. For a more fully-fledged `remove_transparency` method, it'd presumably need to have an option to add a background, and also retain the original mode?

I do also have a use case where I just want to check for particular images that _actually_ have transparent pixels – not to remove the transparency, but to determine an output format (since jpg doesn't support it for example). The examples from SO handle every file I've seen so far, I just can't help but notice I can construct an image where they'd fail.

Patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -915,7 +915,7 @@ def convert(
 
         self.load()
 
-        has_transparency = self.info.get("transparency") is not None
+        has_transparency = "transparency" in self.info
         if not mode and self.mode == "P":
             # determine default mode
             if self.palette:
@@ -1531,6 +1531,24 @@ def getpalette(self, rawmode="RGB"):
             rawmode = mode
         return list(self.im.getpalette(mode, rawmode))
 
+    @property
+    def has_transparency_data(self) -> bool:
+        """
+        Determine if an image has transparency data, whether in the form of an
+        alpha channel, a palette with an alpha channel, or a "transparency" key
+        in the info dictionary.
+
+        Note the image might still appear solid, if all of the values shown
+        within are opaque.
+
+        :returns: A boolean.
+        """
+        return (
+            self.mode in ("LA", "La", "PA", "RGBA", "RGBa")
+            or (self.mode == "P" and self.palette.mode.endswith("A"))
+            or "transparency" in self.info
+        )
+
     def apply_transparency(self):
         """
         If a P mode image has a "transparency" key in the info dictionary,
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
--- a/src/PIL/WebPImagePlugin.py
+++ b/src/PIL/WebPImagePlugin.py
@@ -332,12 +332,7 @@ def _save(im, fp, filename):
     exact = 1 if im.encoderinfo.get("exact") else 0
 
     if im.mode not in _VALID_WEBP_LEGACY_MODES:
-        alpha = (
-            "A" in im.mode
-            or "a" in im.mode
-            or (im.mode == "P" and "transparency" in im.info)
-        )
-        im = im.convert("RGBA" if alpha else "RGB")
+        im = im.convert("RGBA" if im.has_transparency_data else "RGB")
 
     data = _webp.WebPEncode(
         im.tobytes(),


Test patch:
diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py
--- a/Tests/test_file_webp.py
+++ b/Tests/test_file_webp.py
@@ -234,3 +234,13 @@ def test_duration(self, tmp_path):
 
         with Image.open(out_webp) as reloaded:
             assert reloaded.info["duration"] == 1000
+
+    def test_roundtrip_rgba_palette(self, tmp_path):
+        temp_file = str(tmp_path / "temp.webp")
+        im = Image.new("RGBA", (1, 1)).convert("P")
+        assert im.mode == "P"
+        assert im.palette.mode == "RGBA"
+        im.save(temp_file)
+
+        with Image.open(temp_file) as im:
+            assert im.getpixel((0, 0)) == (0, 0, 0, 0)
diff --git a/Tests/test_image.py b/Tests/test_image.py
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -906,6 +906,31 @@ def test_zero_tobytes(self, size):
         im = Image.new("RGB", size)
         assert im.tobytes() == b""
 
+    def test_has_transparency_data(self):
+        for mode in ("1", "L", "P", "RGB"):
+            im = Image.new(mode, (1, 1))
+            assert not im.has_transparency_data
+
+        for mode in ("LA", "La", "PA", "RGBA", "RGBa"):
+            im = Image.new(mode, (1, 1))
+            assert im.has_transparency_data
+
+        # P mode with "transparency" info
+        with Image.open("Tests/images/first_frame_transparency.gif") as im:
+            assert "transparency" in im.info
+            assert im.has_transparency_data
+
+        # RGB mode with "transparency" info
+        with Image.open("Tests/images/rgb_trns.png") as im:
+            assert "transparency" in im.info
+            assert im.has_transparency_data
+
+        # P mode with RGBA palette
+        im = Image.new("RGBA", (1, 1)).convert("P")
+        assert im.mode == "P"
+        assert im.palette.mode == "RGBA"
+        assert im.has_transparency_data
+
     def test_apply_transparency(self):
         im = Image.new("P", (1, 1))
         im.putpalette((0, 0, 0, 1, 1, 1))