Repository navigation
Description
What did you do?
Opened a contiguous 8-bit greyscale TIFF with two samples per pixel (PhotometricInterpretation 1, SamplesPerPixel 2, BitsPerSample (8, 8)) whose ExtraSamples tag is 0 (unspecified data) or 1 (associated alpha). Files of this shape come out of older scan and print tools.
What did you expect to happen?
The file opens, as it does with ExtraSamples 2 (mode LA). With 1 a premultiplied mode such as La would fit; with 0 the extra channel could be dropped or kept without alpha meaning.
What actually happened?
PIL.UnidentifiedImageError: cannot identify image file, raw and LZW alike.
What are your OS, Python and Pillow versions?
- OS: Windows 11 (the same failure was seen on Linux in a Debian trixie container)
- Python: 3.13
- Pillow: 12.3.0
import io
import struct
import PIL
from PIL import Image
def grey_with_extra_sample(extra_samples: int, compression: str | None) -> bytes:
"""A 2-sample greyscale TIFF whose ExtraSamples tag (338) is set to the given value."""
buffer = io.BytesIO()
Image.new("LA", (64, 48), (128, 255)).save(buffer, "TIFF", compression=compression)
data = bytearray(buffer.getvalue())
order = {b"II": "<", b"MM": ">"}[bytes(data[:2])]
(ifd,) = struct.unpack(order + "I", data[4:8])
(count,) = struct.unpack(order + "H", data[ifd : ifd + 2])
for index in range(count):
entry = ifd + 2 + 12 * index
tag, kind, _ = struct.unpack(order + "HHI", data[entry : entry + 8])
if tag == 338:
assert kind == 3 # SHORT, stored inline
data[entry + 8 : entry + 10] = struct.pack(order + "H", extra_samples)
return bytes(data)
raise AssertionError("no ExtraSamples tag written")
print("Pillow", PIL.__version__)
for compression in (None, "tiff_lzw"):
for extra in (0, 1, 2):
try:
with Image.open(io.BytesIO(grey_with_extra_sample(extra, compression))) as image:
image.load()
result = f"ok, mode {image.mode}"
except Exception as error:
result = f"{type(error).__name__}: {error}"
print(f"compression={compression or 'raw'} ExtraSamples={extra}: {result}")Output:
Pillow 12.3.0
compression=raw ExtraSamples=0: UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x...>
compression=raw ExtraSamples=1: UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x...>
compression=raw ExtraSamples=2: ok, mode LA
compression=tiff_lzw ExtraSamples=0: UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x...>
compression=tiff_lzw ExtraSamples=1: UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x...>
compression=tiff_lzw ExtraSamples=2: ok, mode LA
OPEN_INFO in TiffImagePlugin.py has (II|MM, 1, (1,), 1, (8, 8), (2,)) -> ("LA", "LA") but no entry for ExtraSamples 0 or 1 with the same layout. #9514 already ignores unspecified extra samples for PlanarConfiguration 2; this is the contiguous case. Mapping 0 to LA would be wrong, since unspecified data is not alpha, so I would suggest a raw mode LX for mode L, analogous to the existing PX for palette images (unpack the grey byte, skip the extra one). For 1, La seems the natural target.
Would a PR along these lines be welcome? I am happy to prepare one with test images if the direction is right.
Activity
radarhere commentedon Oct 6, 2026
How large are the actual files that you have? If they are small, say under 100kb, it would be good to use them as test files in your PR. If they are not so small, it would be nice to just attach them here if we want to test them manually at any point.
street1983nk commentedon Oct 6, 2026
The real files that hit this are scanned documents on a user's Nextcloud instance, reported downstream to a project of mine; I never had the files themselves, only the tag layout from diagnostics, so there is nothing of size I could share. The script in the report rebuilds that shape exactly, and its output is small: 324 bytes per LZW file, about 6.2 kB raw. Attached are the four failing variants for manual testing (generated with the script above, Pillow 12.3.0).
I see #10140 already proposes OPEN_INFO entries, so I will hold off on my offered PR and leave the LX-versus-LA question for ExtraSamples 0 to you.
grey_extrasamples_repro.zip
radarhere commentedon Oct 7, 2026
I suggested this idea in the PR, and it has now been merged.
street1983nk commentedon Oct 7, 2026
Thank you both for the quick turnaround. The LX route keeps unspecified data out of alpha, exactly what the scans need. I'll retire the downstream workaround once this ships in a release.