Skip to content

Fix BMP save of a P image with an empty palette#9746

Open
gaoflow wants to merge 1 commit into
python-pillow:mainfrom
gaoflow:fix-bmp-empty-palette
Open

Fix BMP save of a P image with an empty palette#9746
gaoflow wants to merge 1 commit into
python-pillow:mainfrom
gaoflow:fix-bmp-empty-palette

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 30, 2026

Copy link
Copy Markdown

Saving a P-mode image whose palette is empty produces a BMP that Pillow cannot reopen:

import io
from PIL import Image

im = Image.new("P", (8, 8))          # empty palette, no putpalette()
im.frombytes(bytes(range(64)))
buf = io.BytesIO()
im.save(buf, "BMP")
buf.seek(0)
Image.open(buf).load()               # OSError: image file is truncated (0 bytes not processed)

PNG, GIF and TIFF all round-trip the same image; only BMP fails, and it fails to read back its own output.

Cause

In BmpImagePlugin._save, the P branch computes colors = len(palette) // 4, which is 0 for an empty palette. The writer then sets biClrUsed = 0 and writes no color table, with the pixel-data offset at 14 + 40 = 54. On read, biClrUsed == 0 at 8 bpp is interpreted as 1 << 8 = 256 entries, so the reader advances the pixel offset by 256 * 4 bytes past a table that was never written — it reads past EOF and reports the file as truncated.

Fix

When the palette is empty, write a full 256-entry table (mirroring the existing L mode branch) so the header is self-consistent and the file reads back. An all-zero table is used rather than a grayscale ramp so the image reopens as P (a ramp would be detected as grayscale and downgraded to L). A populated palette is unaffected.

Tests

test_save_empty_palette parametrizes (1, 1), (7, 5), (8, 8), (16, 16), asserting the written header has a non-zero biClrUsed (pinning the root cause, not just the symptom) and that the image reopens as P with its pixel indices intact. test_save_empty_palette_round_trips_like_other_formats pins the cross-format invariant that BMP reopens the image like PNG/GIF/TIFF do. Both fail on main (OSError / biClrUsed == 0) and pass with the fix; the full Tests/test_file_bmp.py suite stays green (37 passed); ruff format, ruff check and mypy are clean.

Saving a P-mode image whose palette is empty (e.g. Image.new("P", size)
with no putpalette) wrote biClrUsed=0 and no color table. A reader treats
biClrUsed=0 at 8 bits per pixel as 256 entries, so the pixel data offset
points past a color table that was never written and reopening the file
fails with "image file is truncated". PNG, GIF and TIFF all round-trip the
same image.

Write a full 256-entry table when the palette is empty, mirroring the L
mode branch, so the file can be read back. A populated palette is unchanged.
@codspeed-hq

codspeed-hq Bot commented Jun 30, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 331 untouched benchmarks


Comparing gaoflow:fix-bmp-empty-palette (004b6d9) with main (6590b1b)

Open in CodSpeed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants