Skip to content

Fix ColorMap length when saving TIFF images with libtiff - #10178

Merged
radarhere merged 2 commits into
python-pillow:mainfrom
MohammediYunus:fix-libtiff-colormap-stride
Oct 11, 2026
Merged

radarhere merged 2 commits into
python-pillow:mainfrom
MohammediYunus:fix-libtiff-colormap-stride

Conversation

@MohammediYunus

Copy link
Copy Markdown
Contributor

On main, saving a P or PA image through libtiff writes a ColorMap whose green and blue planes are copies of the red plane, shifted by 2 and 4 entries. This is a regression from #10136, which has not been released yet, so I have not added a release note.

from io import BytesIO

from PIL import Image

im = Image.new("P", (1, 1))
im.putpalette((255, 0, 0) * 256)
out = BytesIO()
im.save(out, "TIFF", compression="tiff_lzw")
with Image.open(out) as reloaded:
    print(reloaded.convert("RGB").getpixel((0, 0)))

Pillow 12.3.0 and this PR print (255, 0, 0), but main prints (255, 255, 255). Re-saving an LZW palette TIFF without a compression argument is affected too, since the compression is kept.

#10136 calculates the ColorMap stride from bits_per_sample, which is only set when BitsPerSample has the type TIFF_SHORT.

Pillow/src/encode.c

Lines 987 to 990 in b01a706

if (type == TIFF_SHORT) {
if (key_int == TIFFTAG_BITSPERSAMPLE) {
bits_per_sample = PyLong_AsLong(value);
}

However, BitsPerSample is a core tag, so its type is not looked up, and since _save() passes it as an int, it is autodetected as TIFF_LONG.

Pillow/src/encode.c

Lines 792 to 796 in b01a706

if (type == TIFF_NOTYPE) {
// Autodetect type. Types should not be changed for backwards
// compatibility.
if (PyLong_Check(value)) {
type = TIFF_LONG;

So bits_per_sample stays at 1, and the stride is 2 instead of 256.

Changes proposed in this pull request:

  • Read BitsPerSample for the ColorMap stride whenever it is an int, not only when its type is TIFF_SHORT.
  • In test_palette_save, compare the RGB content, and also test a PA image. Previously, it only checked that the ColorMap had 768 items.
  • In the 16-bit part of test_colormap_size_mismatch, use a non-zero ColorMap and compare its values. Previously, the ColorMap was all zeros and only its length was checked, so the shift was not visible.

Without the src/encode.c change, the P and PA cases of test_palette_save and the libtiff case of test_colormap_size_mismatch fail.

BitsPerSample was only read for the ColorMap stride when its type was
TIFF_SHORT, but it is passed as an int and autodetected as TIFF_LONG.
The stride then stayed at 2, so libtiff stored green and blue as
copies of red, shifted by 2 and 4 entries.
@radarhere radarhere added the TIFF label Oct 11, 2026
Comment thread src/encode.c Outdated
@radarhere

Copy link
Copy Markdown
Member

Thanks very much for catching this before the release!

@radarhere radarhere changed the title Fix ColorMap when saving palette TIFF images with libtiff Fix ColorMap length when saving TIFF images with libtiff Oct 11, 2026
@radarhere
radarhere enabled auto-merge (squash) October 11, 2026 11:36
@radarhere
radarhere merged commit 0e54695 into python-pillow:main Oct 11, 2026
66 of 68 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants