Ignore unspecified extra samples for TIFF separate planar configuration (#9514)

This commit is contained in:
Hugo van Kemenade 2026-03-30 15:54:41 +03:00 committed by GitHub
commit 33e1518cc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 27 additions and 6 deletions

Binary file not shown.

View File

@ -1055,6 +1055,15 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/tiff_strip_planar_16bit_RGBa.tiff") as im:
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png")
def test_separate_planar_extra_samples(self, tmp_path: Path) -> None:
out = tmp_path / "temp.tif"
with Image.open("Tests/images/separate_planar_extra_samples.tiff") as im:
assert im.mode == "L"
im.save(out)
with Image.open(out) as reloaded:
assert reloaded.mode == "L"
@pytest.mark.parametrize("compression", (None, "jpeg"))
def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None:
im = hopper()

View File

@ -1473,28 +1473,34 @@ class TiffImageFile(ImageFile.ImageFile):
logger.debug("- size: %s", self.size)
sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))
if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1:
if len(sample_format) > 1 and max(sample_format) == min(sample_format):
# SAMPLEFORMAT is properly per band, so an RGB image will
# be (1,1,1). But, we don't support per band pixel types,
# and anything more than one band is a uint8. So, just
# take the first element. Revisit this if adding support
# for more exotic images.
sample_format = (1,)
sample_format = (sample_format[0],)
bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,))
extra_tuple = self.tag_v2.get(EXTRASAMPLES, ())
samples_per_pixel = self.tag_v2.get(
SAMPLESPERPIXEL,
3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1,
)
if photo in (2, 6, 8): # RGB, YCbCr, LAB
bps_count = 3
elif photo == 5: # CMYK
bps_count = 4
else:
bps_count = 1
if self._planar_configuration == 2 and extra_tuple and max(extra_tuple) == 0:
# If components are stored separately,
# then unspecified extra components at the end can be ignored
bps_tuple = bps_tuple[: -len(extra_tuple)]
samples_per_pixel -= len(extra_tuple)
extra_tuple = ()
bps_count += len(extra_tuple)
bps_actual_count = len(bps_tuple)
samples_per_pixel = self.tag_v2.get(
SAMPLESPERPIXEL,
3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1,
)
if samples_per_pixel > MAX_SAMPLESPERPIXEL:
# DOS check, samples_per_pixel can be a Long, and we extend the tuple below
@ -1762,6 +1768,12 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
legacy_ifd = im.tag.to_v2()
supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})}
if supplied_tags.get(PLANAR_CONFIGURATION) == 2 and EXTRASAMPLES in supplied_tags:
# If the image used separate component planes,
# then EXTRASAMPLES should be ignored when saving contiguously
if SAMPLESPERPIXEL in supplied_tags:
supplied_tags[SAMPLESPERPIXEL] -= len(supplied_tags[EXTRASAMPLES])
del supplied_tags[EXTRASAMPLES]
for tag in (
# IFD offset that may not be correct in the saved image
EXIFIFD,