From 949652d3e9d4549d3fae16397640019b66caa5d1 Mon Sep 17 00:00:00 2001 From: ybai08 <271969568+ybai08@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:51:21 -0500 Subject: [PATCH 1/2] Fix BigTIFF strip offsets that fit within the entry --- Tests/test_file_tiff.py | 35 +++++++++++++++++++++++++++++++++++ src/PIL/TiffImagePlugin.py | 17 +++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index cf169a3ec5a..7772622eef3 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -155,6 +155,41 @@ def test_bigtiff_save_big_endian_ifd(self, tmp_path: Path) -> None: ifd.load(BytesIO(data)) assert ifd.tagtype[32768] == TiffTags.LONG8 + def test_bigtiff_save_two_strips(self, tmp_path: Path) -> None: + outfile = tmp_path / "temp.tif" + im = hopper("L") + + # Two LONG strip offsets fit within a BigTIFF entry + im.save(outfile, big_tiff=True, tiffinfo={TiffImagePlugin.ROWSPERSTRIP: 64}) + + with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert len(reloaded.tag_v2[TiffImagePlugin.STRIPOFFSETS]) == 2 + assert_image_equal(reloaded, im) + + def test_bigtiff_save_big_endian_strip_offset(self, tmp_path: Path) -> None: + outfile = tmp_path / "temp.tif" + # I;16B is saved as big-endian + im = hopper("I;16B") + + # The single LONG strip offset fits within a BigTIFF entry + im.save(outfile, big_tiff=True) + + with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) + assert reloaded.tag_v2.prefix == b"MM" + assert len(reloaded.tag_v2[TiffImagePlugin.STRIPOFFSETS]) == 1 + assert_image_equal(reloaded, im) + + # Check that the strip offset is upgraded to LONG8 + # when it is too large for a LONG + data = reloaded.tag_v2.tobytes(2**32) + + ifd = TiffImagePlugin.ImageFileDirectory_v2(b"MM\x00\x2b" + b"\x00" * 12) + ifd.load(BytesIO(data)) + assert ifd.tagtype[TiffImagePlugin.STRIPOFFSETS] == TiffTags.LONG8 + assert ifd[TiffImagePlugin.STRIPOFFSETS][0] >= 2**32 + def test_seek_too_large(self) -> None: with pytest.raises(ValueError, match="Unable to seek to frame"): Image.open("Tests/images/seek_too_large.tif") diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6b26b4fb0fb..12be3718c9b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1054,12 +1054,25 @@ def tobytes(self, offset: int = 0) -> bytes: # update strip offset data to point beyond auxiliary data if stripoffsets is not None: tag, typ, count, value, data = entries[stripoffsets] + size, handler = self._load_dispatch[typ] if data: - size, handler = self._load_dispatch[typ] values = [val + offset for val in handler(self, data, self.legacy_api)] data = self._write_dispatch[typ](self, *values) else: - value = self._pack(fmt, self._unpack(fmt, value)[0] + offset) + # The offsets fit within the entry, e.g. two LONG offsets in BigTIFF + values = [ + val + offset + for val in handler(self, value[: size * count], self.legacy_api) + ] + if ( + self._bigtiff + and typ == TiffTags.LONG + and count == 1 + and values[0] >= 2**32 + ): + # Offset is too large for a LONG + typ = TiffTags.LONG8 + value = self._write_dispatch[typ](self, *values).ljust(fmt_size, b"\0") entries[stripoffsets] = tag, typ, count, value, data # pass 2: write entries to file From 633b15f41ba174dd5f5a94a839d304d262e682f3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Oct 2026 12:05:40 +1100 Subject: [PATCH 2/2] Rearrange code --- Tests/test_file_tiff.py | 6 +++++- src/PIL/TiffImagePlugin.py | 14 +++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 7772622eef3..3d6f946728c 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -178,6 +178,10 @@ def test_bigtiff_save_big_endian_strip_offset(self, tmp_path: Path) -> None: with Image.open(outfile) as reloaded: assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2.prefix == b"MM" + assert reloaded.tag_v2._bigtiff is True + assert ( + reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPOFFSETS] == TiffTags.LONG + ) assert len(reloaded.tag_v2[TiffImagePlugin.STRIPOFFSETS]) == 1 assert_image_equal(reloaded, im) @@ -188,7 +192,7 @@ def test_bigtiff_save_big_endian_strip_offset(self, tmp_path: Path) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2(b"MM\x00\x2b" + b"\x00" * 12) ifd.load(BytesIO(data)) assert ifd.tagtype[TiffImagePlugin.STRIPOFFSETS] == TiffTags.LONG8 - assert ifd[TiffImagePlugin.STRIPOFFSETS][0] >= 2**32 + assert ifd[TiffImagePlugin.STRIPOFFSETS][0] > 2**32 def test_seek_too_large(self) -> None: with pytest.raises(ValueError, match="Unable to seek to frame"): diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 12be3718c9b..be8dd435e7f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1055,22 +1055,22 @@ def tobytes(self, offset: int = 0) -> bytes: if stripoffsets is not None: tag, typ, count, value, data = entries[stripoffsets] size, handler = self._load_dispatch[typ] + values = [ + val + offset for val in handler(self, data or value, self.legacy_api) + ] if data: - values = [val + offset for val in handler(self, data, self.legacy_api)] data = self._write_dispatch[typ](self, *values) else: - # The offsets fit within the entry, e.g. two LONG offsets in BigTIFF - values = [ - val + offset - for val in handler(self, value[: size * count], self.legacy_api) - ] + # The offsets fit within the entry + # May be one offset, or two LONG offsets in BigTIFF if ( self._bigtiff and typ == TiffTags.LONG and count == 1 and values[0] >= 2**32 ): - # Offset is too large for a LONG + # If a single offset is too large for a LONG, + # in BigTIFF there is room to change the entry to LONG8 typ = TiffTags.LONG8 value = self._write_dispatch[typ](self, *values).ljust(fmt_size, b"\0") entries[stripoffsets] = tag, typ, count, value, data