Repository: python-pillow/Pillow

Problem statement:
BmpImagePlugin - Unsupported 32bpp DIBs with Alpha
### What did you do?
Attempted to read the attached 32bpp DIB with `Image.open()`

(I've plopped it in a ZIP because GitHub doesn't like me just chucking the DIB itself in here)
[sampledib.zip](https://github.com/user-attachments/files/18119317/sampledib.zip)

### What did you expect to happen?
The image reads as `RGBA`

### What actually happened?
The image reads as `RGB`

### What are your OS, Python and Pillow versions?
Locally built Pillow from https://github.com/python-pillow/Pillow/commit/d66c51a35663247357bf6b625ef093df7ea12a45
* OS: Debian 12
* Python: Pillow 11.1.0.dev0
* Pillow: Python 3.11.2 (main, Sep 14 2024, 03:00:30) [GCC 12.2.0]

```text
--------------------------------------------------------------------
Pillow 11.1.0.dev0
Python 3.11.2 (main, Sep 14 2024, 03:00:30) [GCC 12.2.0]
--------------------------------------------------------------------
Python executable is /home/rory/source/repos/xfce-winxp-tc/tools/theme/.venv/bin/python3
Environment Python files loaded from /home/rory/source/repos/xfce-winxp-tc/tools/theme/.venv
System Python files loaded from /usr
--------------------------------------------------------------------
Python Pillow modules loaded from /home/rory/source/repos/xfce-winxp-tc/tools/theme/.venv/lib/python3.11/site-packages/PIL
Binary Pillow modules loaded from /home/rory/source/repos/xfce-winxp-tc/tools/theme/.venv/lib/python3.11/site-packages/PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 11.1.0.dev0
--- TKINTER support ok, loaded 8.6
--- FREETYPE2 support ok, loaded 2.12.1
--- LITTLECMS2 support ok, loaded 2.14
--- WEBP support ok, loaded 1.2.4
--- JPEG support ok, compiled for libjpeg-turbo 2.1.5
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.0
--- ZLIB (PNG/ZIP) support ok, loaded 1.2.13
--- LIBTIFF support ok, loaded 4.5.0
*** RAQM (Bidirectional Text) support not installed
*** LIBIMAGEQUANT (Quantization method) support not installed
--- XCB (X protocol) support ok
--------------------------------------------------------------------
```



Hints:
Sorry to bother you all with such a niche problem. :sweat_smile: 

For context, the attached bitmap (DIB) comes from one of the themes in Windows XP - I am trying to look into writing a general tool for converting .MSSTYLES based themes to various Linux toolkits.

I pinpointed this line of interest in `BmpImagePlugin`: https://github.com/python-pillow/Pillow/blob/main/src/PIL/BmpImagePlugin.py#L245

Removing this check for 22 byte header does resolve the issue for me, however it breaks the 32bpp bitmap test case. Unfortunately it seems like the alpha channel behaviour is undocumented, at least I cannot find anything. Both images use the 40-byte BITMAPINFOHEADER, and the only real difference is `biSizeImage` being set to 0 in the broken bitmap. Not something I think can be relied on to change how the data should be interpreted.

I don't think this is something easily solvable via parsing the format alone, so I have a fork of Pillow to include `BMPA` and `DIBA` as additional formats for explicitly hinting that the alpha channel is to be read.

I'll submit a corresponding PR, though my solution might be rubbish - I don't use Python all that much. :stuck_out_tongue_closed_eyes: 
For context, https://github.com/python-pillow/Pillow/pull/1125 is the PR that introduced that line of code.

Can I ask - how do you know the image should be RGBA? Googling, I found https://jumpshare.com/viewer/bmp, and it showed no transparency when given the file.
I found https://learn.microsoft.com/en-us/windows/win32/wmdm/-bitmapinfoheader, which describes 32 bitcount as
> The bitmap has a maximum of 2^32 colors. If the biCompression member is BI_RGB, the bmiColors member is NULL. Each DWORD in the bitmap array represents the relative intensities of blue, green, and red, respectively, for a pixel. The high byte in each DWORD is not used.

So it sounds like RGB is the intention.
> Can I ask - how do you know the image should be RGBA? Googling, I found https://jumpshare.com/viewer/bmp, and it showed no transparency when given the file.

Because this DIB comes directly from the resource file `luna.msstyles` - the theme engine does actually use the alpha channel (they moved to using PNGs in Windows Vista). All themes on Windows XP make use of RGBA bitmaps in the same way.

Here's a comparison between the current read and what is intended:
![BLUE_RADIOBUTTON25_BMP](https://github.com/user-attachments/assets/f7ca1609-1e58-4b8e-b9f2-14729e1e3b57) ![BLUE_RADIOBUTTON25_BMP](https://github.com/user-attachments/assets/5be21cf1-2040-48ac-ab84-fa9d92d758c2)

I know the documentation does not say anything about it, I'm not sure if it was ever formally documented or whether it's just modern MSDN missing stuff. From searching around there's old forum posts and pages on CodeProject (RIP) about these 32bpp bitmaps. I think potentially it is possible to create them in Photoshop, but I'm not sure if that's still the case in newer versions.

The question I was trying to ask is - what program are you using to view this file that shows transparency?

Should I understand your response to mean that you're not viewing them in a program, and your expectation is based on the fact that Windows loads this data and then you see the result in the OS? Not just the transparent corners, but also a transparent inside under some circumstances?
You caught me just before I finished writing this update. :stuck_out_tongue: 

In that post I compared between `DIB` in Pillow, and `DIBA` in my branch - so it was based off the expectation of how Windows handles the data. I have just had a look out of curiosity to see if what I read about Photoshop is true, and it does appear to be the case that Photoshop (CS2 here) also recognises the alpha channel:

![image](https://github.com/user-attachments/assets/90a07221-43ba-4a12-9487-22ac9f1518a5)

Hmm. I'm concerned about interpreting data that the documentation says to ignore, because that's what third-party software would follow when creating images, and it's possible that what they write to this fourth channel may not actually work as an alpha channel. So it might fix your image, but it might also break others.

What do you think of this code, that will work with Pillow as-is to read your image as RGBA?
```python
from PIL import Image, _binary, BmpImagePlugin
filename = "BLUE_RADIOBUTTON25_BMP.bmp"
im = Image.open(filename)

if im.format == "DIB" and im.info["compression"] == BmpImagePlugin.BmpImageFile.COMPRESSIONS["RAW"]:
    bits = None
    with open(filename, "rb") as fp:
        header_size = _binary.i32le(fp.read(4))
        if header_size == 40:
            header_data = fp.read(header_size - 4)
            bits = _binary.i16le(header_data, 10)
    if bits == 32:
        im._mode = "RGBA"
        
        args = list(im.tile[0].args)
        args[0] = "BGRA"
        im.tile = [im.tile[0]._replace(args=tuple(args))]

im.save("out.png")
```
> Hmm. I'm concerned about interpreting data that the documentation says to ignore, because that's what third-party software would follow when creating images, and it's possible that what they write to this fourth channel may not actually work as an alpha channel. So it might fix your image, but it might also break others.

This was why I opted to add the extra formats to act as 'hints', rather than trying to guess what the interpretation should be - so it cannot break existing images: https://github.com/rozniak/Pillow/commit/79683d3a79223f9c891c7a06967576bb776f0f7b

> What do you think of this code, that will work with Pillow as-is to read your image as RGBA?

It works perfectly. :smile: I mean it's up to you really, I suppose it might not be a very common type of bitmap for people to read so I don't mind working with this snippet in my own project. I did try to keep the change minimal and to avoid breaking any existing images according to spec, depends whether you think it's worth having or not. I suppose there is always this issue thread for future reference otherwise. :stuck_out_tongue: 


Patch:
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
--- a/src/PIL/BmpImagePlugin.py
+++ b/src/PIL/BmpImagePlugin.py
@@ -48,6 +48,8 @@
     32: ("RGB", "BGRX"),
 }
 
+USE_RAW_ALPHA = False
+
 
 def _accept(prefix: bytes) -> bool:
     return prefix[:2] == b"BM"
@@ -242,7 +244,9 @@ def _bitmap(self, header: int = 0, offset: int = 0) -> None:
                 msg = "Unsupported BMP bitfields layout"
                 raise OSError(msg)
         elif file_info["compression"] == self.COMPRESSIONS["RAW"]:
-            if file_info["bits"] == 32 and header == 22:  # 32-bit .cur offset
+            if file_info["bits"] == 32 and (
+                header == 22 or USE_RAW_ALPHA  # 32-bit .cur offset
+            ):
                 raw_mode, self._mode = "BGRA", "RGBA"
         elif file_info["compression"] in (
             self.COMPRESSIONS["RLE8"],


Test patch:
diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -230,3 +230,13 @@ def test_offset() -> None:
     # to exclude the palette size from the pixel data offset
     with Image.open("Tests/images/pal8_offset.bmp") as im:
         assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp")
+
+
+def test_use_raw_alpha(monkeypatch: pytest.MonkeyPatch) -> None:
+    with Image.open("Tests/images/bmp/g/rgb32.bmp") as im:
+        assert im.info["compression"] == BmpImagePlugin.BmpImageFile.COMPRESSIONS["RAW"]
+        assert im.mode == "RGB"
+
+    monkeypatch.setattr(BmpImagePlugin, "USE_RAW_ALPHA", True)
+    with Image.open("Tests/images/bmp/g/rgb32.bmp") as im:
+        assert im.mode == "RGBA"