You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
BITMAPCOREHEADER DIB resources decoded incorrectly (wrong color table stride, wrong row stride for 1-bit) #9466
When loading raw DIB (device-independent bitmap) data embedded in Windows PE resource sections (e.g. cards.dll), Pillow misreads bitmaps that use the old OS/2 BITMAPCOREHEADER format (12-byte header, bcSize=12).
Two bugs
1. 4-bit paletted images
Color table is read as RGBQUAD (4 bytes/entry) instead of RGBTRIPLE (3 bytes/entry), shifting the pixel data offset by 16 bytes and producing wrong colors.
2. 1-bit monochrome images
Row stride calculation is wrong, producing a solid black stripe ~17px wide on the left of every decoded image.
Repro
Extract any bitmap resource from Windows XP cards.dll using pefile, write the raw bytes (without a BITMAPFILEHEADER) and open with:
This runs when the BITMAPFILEHEADER pixel offset doesn't account for the palette (i.e., offset == 14 + header_size, just past the DIB header). It then tries to add the palette byte size — but uses a hardcoded 4 bytes/entry (RGBQUAD), which is correct for BITMAPINFOHEADER but wrong for BITMAPCOREHEADER, which uses RGBTRIPLE (3 bytes/entry).
For a 4bpp image (16 colors): the correction adds 4×16 = 64 bytes instead of the correct 3×16 = 48 bytes, causing Pillow to seek 16 bytes too far, reading wrong pixel data.
The fix is to use file_info["palette_padding"] (already correctly set to 3 for BITMAPCOREHEADER at line 103):
pal8os2.bmp has a correct BITMAPFILEHEADER pixel offset (properly accounting for the full 256-color RGBTRIPLE table), so offset != 14 + header_size and the buggy correction code path is never reached.
The bug only triggers when the file header's pixel offset equals exactly 14 + header_size — a value written by software that omits the color table size from the offset calculation.
The original code was added in #5899. I was taking advice from a blog post that apparently didn't consider BITMAPCOREHEADER. I've created #9472 to resolve this.
Activity
radarhere commentedon Mar 13, 2026
Would you be able to link to an image that demonstrates the problem for those of us who aren't on Windows XP?
In our test suite, we check https://github.com/python-pillow/Pillow/raw/refs/heads/main/Tests/images/bmp/g/pal8os2.bmp, which looks correct when I save it as a PNG using Pillow.
elemental-admin-dev commentedon Mar 15, 2026
Thanks for the quick response, @radarhere! I've been able to isolate the exact bug.
Root Cause
The issue is in
BmpImagePlugin.pyat the offset correction block:This runs when the
BITMAPFILEHEADERpixel offset doesn't account for the palette (i.e.,offset == 14 + header_size, just past the DIB header). It then tries to add the palette byte size — but uses a hardcoded4bytes/entry (RGBQUAD), which is correct forBITMAPINFOHEADERbut wrong forBITMAPCOREHEADER, which uses RGBTRIPLE (3 bytes/entry).For a 4bpp image (16 colors): the correction adds
4×16 = 64bytes instead of the correct3×16 = 48bytes, causing Pillow to seek 16 bytes too far, reading wrong pixel data.The fix is to use
file_info["palette_padding"](already correctly set to3for BITMAPCOREHEADER at line 103):Minimal Reproducer
This script creates a 16×16 BITMAPCOREHEADER BMP where each row should be a distinct color, demonstrating the wrong colors Pillow reads:
Output on Pillow 12.1.1:
Why
pal8os2.bmppassespal8os2.bmphas a correctBITMAPFILEHEADERpixel offset (properly accounting for the full 256-color RGBTRIPLE table), sooffset != 14 + header_sizeand the buggy correction code path is never reached.The bug only triggers when the file header's pixel offset equals exactly
14 + header_size— a value written by software that omits the color table size from the offset calculation.radarhere commentedon Mar 18, 2026
Thanks for the explanation.
The original code was added in #5899. I was taking advice from a blog post that apparently didn't consider BITMAPCOREHEADER. I've created #9472 to resolve this.