From a87443759085fd03ad4e472acd89b1775e7f5a52 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Oct 2026 15:35:56 +1100 Subject: [PATCH 1/2] Keep first TIFF tag, fixing C bits mismatch --- Tests/test_file_libtiff.py | 110 +++++++++++++++++++------------------ src/PIL/TiffImagePlugin.py | 2 + 2 files changed, 60 insertions(+), 52 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 79bba46d516..fbcae452b12 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1323,61 +1323,67 @@ def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None: with pytest.raises(OSError, match="decoder error -9"): im.load() - @pytest.mark.parametrize( - "data", - ( - # 16x16 8-bit greyscale tile, with a PhotometricInterpretation tag that - # has two values. Pillow reads it as YCbCr (rawmode RGBX, 4 bytes/pixel) - # while libtiff ignores it and decodes 1 byte/pixel. - ( - b"SUkqAAgAAAAKAAABBAABAAAAEAAAAAEBBAABAAAAEAAAAAIBAwABAAAACAAA" - b"AAMBAwABAAAACAAAAAYBAwACAAAABgAGABUBAwABAAAAAQAAAEIBBAABAAAA" - b"EAAAAEMBBAABAAAAEAAAAEQBBAABAAAAhgAAAEUBBAABAAAACwEAAAAAAAB4" - b"nAEAAf/+AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUm" - b"JygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJT" - b"VFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+A" - b"gYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6yt" - b"rq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna" - b"29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+foAAQIDBJ9Bepo=" - ), - # 16x16 strip with duplicated BitsPerSample and PhotometricInterpretation - # tags. libtiff keeps the first (8-bit YCbCr, read as 4 bytes/pixel RGBA) - # while Pillow keeps the last (16-bit RGB, rawmode RGB;16N, 6 bytes/pixel). - ( - b"SUkqAAgAAAAMAAABAwABAAAAEAAAAAEBAwABAAAAEAAAAAIBAwADAAAAugEA" - b"AAIBAwADAAAAwAEAAAMBAwABAAAACAAAAAYBAwABAAAABgAAAAYBAwABAAAA" - b"AgAAABEBBAABAAAAngAAABUBAwABAAAAAwAAABYBAwABAAAAEAAAABcBBAAB" - b"AAAAHAEAABICAwACAAAAAQABAAAAAAB4nGNgZGJmYWVj5+Dk4ubh5eMXEBQS" - b"FhEVE5eQlJKWkZWTV1BUUlZRVVPX0NTS1tHV0zcwNDI2MTUzt7C0sraxtbN3" - b"cHRydnF1c/fw9PL28fXzDwgMCg4JDQuPiIyKjomNi09ITEpOSU1Lz8jMys7J" - b"zcsvKCwqLiktK6+orKquqa2rb2hsam5pbWvv6Ozq7unt658wcdLkKVOnTZ8x" - b"c9bsOXPnzV+wcNHiJUuXLV+xctXqNWvXrd+wcdPmLVu3bd+xc9fuPXv37T9w" - b"8NDhI0ePHT9x8tTpM2fPnb9w8dLlK1evXb9x89btO3fv3X/w8NHjJ0+fPX/x" - b"8tXrN2/fvf/w8dPnL1+/ff/x89fvP3///WcY9f+I9j8AoGJ+kAgACAAIABAA" - b"EAAQAA==" - ), - # 4x2 PackBits strip with duplicated BitsPerSample, Photometric - # Interpretation and SamplesPerPixel tags. libtiff keeps the first - # (8-bit YCbCr, read as 4 bytes/pixel RGBA) while Pillow keeps the last - # (6 channel RGB, rawmode RGBAXX, 6 bytes/pixel). - ( - b"SUkqAAgAAAAPAAABBAABAAAABAAAAAEBBAABAAAAAgAAAAIBAwADAAAAwgAA" - b"AAIBAwAGAAAAyAAAAAMBAwABAAAABYAAAAYBAwABAAAABgAAAAYBAwABAAAA" - b"AgAAABEBBAABAAAA2gAAABUBAwABAAAAAwAAABUBAwABAAAABgAAABYBBAAB" - b"AAAAAgAAABcBBAABAAAAGQAAABwBAwABAAAAAQAAAFIBAwADAAAA1AAAABIC" - b"AwACAAAAAQABAAAAAAAIAAgACAAIAAgACAAIAAgACAACAAAAAAAXAAECAwQF" - b"BgcICQoLDA0ODxAREhMUFRYX" - ), - ), - ) - @pytest.mark.filterwarnings("ignore:Metadata Warning") - def test_unpacker_row_wider_than_libtiff_row(self, data: bytes) -> None: + def test_unpacker_row_wider_than_libtiff_row(self) -> None: + # 16x16 8-bit greyscale tile, with a PhotometricInterpretation tag that + # has two values. Pillow reads it as YCbCr (rawmode RGBX, 4 bytes/pixel) + # while libtiff ignores it and decodes 1 byte/pixel. + data = ( + b"SUkqAAgAAAAKAAABBAABAAAAEAAAAAEBBAABAAAAEAAAAAIBAwABAAAACAAA" + b"AAMBAwABAAAACAAAAAYBAwACAAAABgAGABUBAwABAAAAAQAAAEIBBAABAAAA" + b"EAAAAEMBBAABAAAAEAAAAEQBBAABAAAAhgAAAEUBBAABAAAACwEAAAAAAAB4" + b"nAEAAf/+AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUm" + b"JygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJT" + b"VFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+A" + b"gYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6yt" + b"rq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna" + b"29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+foAAQIDBJ9Bepo=" + ) + with pytest.warns( + UserWarning, + match="Metadata Warning, tag 262 had too many entries: 2, expected 1", + ): + with Image.open(io.BytesIO(base64.b64decode(data))) as im: + with pytest.raises(OSError, match="decoder error -2"): + im.load() + + # 16x16 strip with duplicated + # BitsPerSample and PhotometricInterpretation tags. + # The first reports 8-bit YCbCr, read as 4 bytes/pixel RGBA, + # and the last reports 16-bit RGB, rawmode RGB;16N, 6 bytes/pixel. + data = ( + b"SUkqAAgAAAAMAAABAwABAAAAEAAAAAEBAwABAAAAEAAAAAIBAwADAAAAugEA" + b"AAIBAwADAAAAwAEAAAMBAwABAAAACAAAAAYBAwABAAAABgAAAAYBAwABAAAA" + b"AgAAABEBBAABAAAAngAAABUBAwABAAAAAwAAABYBAwABAAAAEAAAABcBBAAB" + b"AAAAHAEAABICAwACAAAAAQABAAAAAAB4nGNgZGJmYWVj5+Dk4ubh5eMXEBQS" + b"FhEVE5eQlJKWkZWTV1BUUlZRVVPX0NTS1tHV0zcwNDI2MTUzt7C0sraxtbN3" + b"cHRydnF1c/fw9PL28fXzDwgMCg4JDQuPiIyKjomNi09ITEpOSU1Lz8jMys7J" + b"zcsvKCwqLiktK6+orKquqa2rb2hsam5pbWvv6Ozq7unt658wcdLkKVOnTZ8x" + b"c9bsOXPnzV+wcNHiJUuXLV+xctXqNWvXrd+wcdPmLVu3bd+xc9fuPXv37T9w" + b"8NDhI0ePHT9x8tTpM2fPnb9w8dLlK1evXb9x89btO3fv3X/w8NHjJ0+fPX/x" + b"8tXrN2/fvf/w8dPnL1+/ff/x89fvP3///WcY9f+I9j8AoGJ+kAgACAAIABAA" + b"EAAQAA==" + ) + with Image.open(io.BytesIO(base64.b64decode(data))) as im: + im.load() + + # 4x2 PackBits strip with duplicated + # BitsPerSample, Photometric Interpretation and SamplesPerPixel tags. + # The first reports 8-bit YCbCr, read as 4 bytes/pixel RGBA, + # and the last reports 6 channel RGB, rawmode RGBAXX, 6 bytes/pixel. + data = ( + b"SUkqAAgAAAAPAAABBAABAAAABAAAAAEBBAABAAAAAgAAAAIBAwADAAAAwgAA" + b"AAIBAwAGAAAAyAAAAAMBAwABAAAABYAAAAYBAwABAAAABgAAAAYBAwABAAAA" + b"AgAAABEBBAABAAAA2gAAABUBAwABAAAAAwAAABUBAwABAAAABgAAABYBBAAB" + b"AAAAAgAAABcBBAABAAAAGQAAABwBAwABAAAAAQAAAFIBAwADAAAA1AAAABIC" + b"AwACAAAAAQABAAAAAAAIAAgACAAIAAgACAAIAAgACAACAAAAAAAXAAECAwQF" + b"BgcICQoLDA0ODxAREhMUFRYX" + ) + # The unpacker chosen by Pillow reads more bytes per row than libtiff # decoded, so the decoder must reject the image rather than read past # the end of its buffer - with Image.open(io.BytesIO(base64.b64decode(data))) as im: - with pytest.raises(OSError, match="decoder error -2"): - im.load() + with pytest.raises(SyntaxError, match="unknown pixel mode"): + TiffImagePlugin.TiffImageFile(io.BytesIO(base64.b64decode(data))) @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3a3db4ffa35..b74ea9ce082 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -907,6 +907,8 @@ def load(self, fp: IO[bytes]) -> None: if self._bigtiff else self._unpack("HHL4s", self._ensure_read(fp, 12)) ) + if tag in self._tagdata: + continue tagname = TiffTags.lookup(tag, self.group).name typname = TYPES.get(typ, "unknown") From ec12f0c1f060d91caeb33cf85e4b87d7f77e87fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Oct 2026 15:55:06 +1100 Subject: [PATCH 2/2] Ignore PhotometricInterpretation tag with too many entries --- Tests/test_file_libtiff.py | 72 +++++++++++++++++--------------------- src/PIL/TiffImagePlugin.py | 11 +++--- 2 files changed, 39 insertions(+), 44 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index fbcae452b12..af7d2d9f525 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1324,47 +1324,41 @@ def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None: im.load() def test_unpacker_row_wider_than_libtiff_row(self) -> None: - # 16x16 8-bit greyscale tile, with a PhotometricInterpretation tag that - # has two values. Pillow reads it as YCbCr (rawmode RGBX, 4 bytes/pixel) - # while libtiff ignores it and decodes 1 byte/pixel. - data = ( - b"SUkqAAgAAAAKAAABBAABAAAAEAAAAAEBBAABAAAAEAAAAAIBAwABAAAACAAA" - b"AAMBAwABAAAACAAAAAYBAwACAAAABgAGABUBAwABAAAAAQAAAEIBBAABAAAA" - b"EAAAAEMBBAABAAAAEAAAAEQBBAABAAAAhgAAAEUBBAABAAAACwEAAAAAAAB4" - b"nAEAAf/+AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUm" - b"JygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJT" - b"VFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+A" - b"gYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6yt" - b"rq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna" - b"29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+foAAQIDBJ9Bepo=" - ) - with pytest.warns( - UserWarning, - match="Metadata Warning, tag 262 had too many entries: 2, expected 1", + for data in ( + # 16x16 8-bit greyscale tile, with a PhotometricInterpretation tag that + # has two values. Pillow reads it as YCbCr (rawmode RGBX, 4 bytes/pixel) + # while libtiff ignores it and decodes 1 byte/pixel. + ( + b"SUkqAAgAAAAKAAABBAABAAAAEAAAAAEBBAABAAAAEAAAAAIBAwABAAAACAAA" + b"AAMBAwABAAAACAAAAAYBAwACAAAABgAGABUBAwABAAAAAQAAAEIBBAABAAAA" + b"EAAAAEMBBAABAAAAEAAAAEQBBAABAAAAhgAAAEUBBAABAAAACwEAAAAAAAB4" + b"nAEAAf/+AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUm" + b"JygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJT" + b"VFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+A" + b"gYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6yt" + b"rq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna" + b"29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+foAAQIDBJ9Bepo=" + ), + # 16x16 strip with duplicated + # BitsPerSample and PhotometricInterpretation tags. + # The first reports 8-bit YCbCr, read as 4 bytes/pixel RGBA, + # and the last reports 16-bit RGB, rawmode RGB;16N, 6 bytes/pixel. + ( + b"SUkqAAgAAAAMAAABAwABAAAAEAAAAAEBAwABAAAAEAAAAAIBAwADAAAAugEA" + b"AAIBAwADAAAAwAEAAAMBAwABAAAACAAAAAYBAwABAAAABgAAAAYBAwABAAAA" + b"AgAAABEBBAABAAAAngAAABUBAwABAAAAAwAAABYBAwABAAAAEAAAABcBBAAB" + b"AAAAHAEAABICAwACAAAAAQABAAAAAAB4nGNgZGJmYWVj5+Dk4ubh5eMXEBQS" + b"FhEVE5eQlJKWkZWTV1BUUlZRVVPX0NTS1tHV0zcwNDI2MTUzt7C0sraxtbN3" + b"cHRydnF1c/fw9PL28fXzDwgMCg4JDQuPiIyKjomNi09ITEpOSU1Lz8jMys7J" + b"zcsvKCwqLiktK6+orKquqa2rb2hsam5pbWvv6Ozq7unt658wcdLkKVOnTZ8x" + b"c9bsOXPnzV+wcNHiJUuXLV+xctXqNWvXrd+wcdPmLVu3bd+xc9fuPXv37T9w" + b"8NDhI0ePHT9x8tTpM2fPnb9w8dLlK1evXb9x89btO3fv3X/w8NHjJ0+fPX/x" + b"8tXrN2/fvf/w8dPnL1+/ff/x89fvP3///WcY9f+I9j8AoGJ+kAgACAAIABAA" + b"EAAQAA==" + ), ): with Image.open(io.BytesIO(base64.b64decode(data))) as im: - with pytest.raises(OSError, match="decoder error -2"): - im.load() - - # 16x16 strip with duplicated - # BitsPerSample and PhotometricInterpretation tags. - # The first reports 8-bit YCbCr, read as 4 bytes/pixel RGBA, - # and the last reports 16-bit RGB, rawmode RGB;16N, 6 bytes/pixel. - data = ( - b"SUkqAAgAAAAMAAABAwABAAAAEAAAAAEBAwABAAAAEAAAAAIBAwADAAAAugEA" - b"AAIBAwADAAAAwAEAAAMBAwABAAAACAAAAAYBAwABAAAABgAAAAYBAwABAAAA" - b"AgAAABEBBAABAAAAngAAABUBAwABAAAAAwAAABYBAwABAAAAEAAAABcBBAAB" - b"AAAAHAEAABICAwACAAAAAQABAAAAAAB4nGNgZGJmYWVj5+Dk4ubh5eMXEBQS" - b"FhEVE5eQlJKWkZWTV1BUUlZRVVPX0NTS1tHV0zcwNDI2MTUzt7C0sraxtbN3" - b"cHRydnF1c/fw9PL28fXzDwgMCg4JDQuPiIyKjomNi09ITEpOSU1Lz8jMys7J" - b"zcsvKCwqLiktK6+orKquqa2rb2hsam5pbWvv6Ozq7unt658wcdLkKVOnTZ8x" - b"c9bsOXPnzV+wcNHiJUuXLV+xctXqNWvXrd+wcdPmLVu3bd+xc9fuPXv37T9w" - b"8NDhI0ePHT9x8tTpM2fPnb9w8dLlK1evXb9x89btO3fv3X/w8NHjJ0+fPX/x" - b"8tXrN2/fvf/w8dPnL1+/ff/x89fvP3///WcY9f+I9j8AoGJ+kAgACAAIABAA" - b"EAAQAA==" - ) - with Image.open(io.BytesIO(base64.b64decode(data))) as im: - im.load() + im.load() # 4x2 PackBits strip with duplicated # BitsPerSample, Photometric Interpretation and SamplesPerPixel tags. diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index b74ea9ce082..35b490bd4e3 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -765,11 +765,12 @@ def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None: (dest[tag],) = values except ValueError: # We've got a builtin tag with 1 expected entry - warnings.warn( - f"Metadata Warning, tag {tag} had too many entries: " - f"{len(values)}, expected 1" - ) - dest[tag] = values[0] + if tag != PHOTOMETRIC_INTERPRETATION: + warnings.warn( + f"Metadata Warning, tag {tag} had too many entries: " + f"{len(values)}, expected 1" + ) + dest[tag] = values[0] else: # Spec'd length > 1 or undefined