Fix BMP save of a P image with an empty palette - #9746
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
|
You have assert that if you save a P mode image as a BMP, it should be re-opened as a P mode image. BmpImagePlugin is actively trying to move to the simpler mode though. Pillow/src/PIL/BmpImagePlugin.py Lines 275 to 283 in fc7cec1 I don't think that all image formats need to behave exactly the same. The PNG format actually has a 'Color type' that distinguishes whether the data is grayscale or palette. The BMP format doesn't have that information. So for a P mode image with an empty palette, I would be inclined to save only 1 palette entry, so that the final file is as small as possible. |
A "P" image with an empty palette was written with biClrUsed=0 and no color table. A reader treats biClrUsed=0 at 8bpp as 256 entries, so the pixel offset pointed past a table that was never written and the file could not be reopened. Write a single palette entry so the header is self-consistent.
004b6d9 to
c78c84c
Compare
|
Fair point on both. Switched to a single palette entry — 122 bytes now instead of 1142 — and dropped the mode assertion and the cross-format test, since BMP has no way to record the grayscale/palette distinction and the reader is right to simplify. It reopens as The test now just pins |
Saving a
P-mode image whose palette is empty produces a BMP that Pillow cannot reopen: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, thePbranch computescolors = len(palette) // 4, which is0for an empty palette. The writer then setsbiClrUsed = 0and writes no color table, with the pixel-data offset at14 + 40 = 54. On read,biClrUsed == 0at 8 bpp is interpreted as1 << 8 = 256entries, so the reader advances the pixel offset by256 * 4bytes 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
Lmode 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 asP(a ramp would be detected as grayscale and downgraded toL). A populated palette is unaffected.Tests
test_save_empty_paletteparametrizes(1, 1),(7, 5),(8, 8),(16, 16), asserting the written header has a non-zerobiClrUsed(pinning the root cause, not just the symptom) and that the image reopens asPwith its pixel indices intact.test_save_empty_palette_round_trips_like_other_formatspins the cross-format invariant that BMP reopens the image like PNG/GIF/TIFF do. Both fail onmain(OSError/biClrUsed == 0) and pass with the fix; the fullTests/test_file_bmp.pysuite stays green (37 passed);ruff format,ruff checkandmypyare clean.