镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

TIFF: greyscale with one extra sample fails to open when ExtraSamples is 0 (unspecified) or 1 (associated alpha) #10139

Closed

Description

@street1983nk

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

  1. radarhere commented on Oct 6, 2026

    @radarhere
    Member

    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.

  2. street1983nk commented on Oct 6, 2026

    @street1983nk
    Author

    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

  3. radarhere commented on Oct 7, 2026

    @radarhere
    Member

    I would suggest a raw mode LX for mode L

    I suggested this idea in the PR, and it has now been merged.

  4. street1983nk commented on Oct 7, 2026

    @street1983nk
    Author

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions