diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 44da25295ef..af766022b5c 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -108,10 +108,6 @@ def test_bigtiff(self, tmp_path: Path) -> None: assert_image_equal_tofile(im, "Tests/images/hopper.tif") with Image.open("Tests/images/hopper_bigtiff.tif") as im: - # The data type of this file's StripOffsets tag is LONG8, - # which is not yet supported for offset data when saving multiple frames. - del im.tag_v2[273] - outfile = str(tmp_path / "temp.tif") im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) @@ -732,6 +728,20 @@ def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: with Image.open(mp) as reread: assert reread.n_frames == 3 + def test_fixoffsets(self) -> None: + b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") + with TiffImagePlugin.AppendingTiffWriter(b) as a: + b.seek(0) + a.fixOffsets(1, isShort=True) + + b.seek(0) + a.fixOffsets(1, isLong=True) + + # Neither short nor long + b.seek(0) + with pytest.raises(RuntimeError): + a.fixOffsets(1) + def test_saving_icc_profile(self, tmp_path: Path) -> None: # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index d4c46a79778..a655bb741e7 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2121,13 +2121,24 @@ def skipIFDs(self) -> None: def write(self, data: Buffer, /) -> int: return self.f.write(data) - def readShort(self) -> int: - (value,) = struct.unpack(self.shortFmt, self.f.read(2)) + def _fmt(self, field_size: int) -> str: + try: + return {2: "H", 4: "L", 8: "Q"}[field_size] + except KeyError: + msg = "offset is not supported" + raise RuntimeError(msg) + + def _read(self, field_size: int) -> int: + (value,) = struct.unpack( + self.endian + self._fmt(field_size), self.f.read(field_size) + ) return value + def readShort(self) -> int: + return self._read(2) + def readLong(self) -> int: - (value,) = struct.unpack(self.longFmt, self.f.read(4)) - return value + return self._read(4) @staticmethod def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: @@ -2140,15 +2151,18 @@ def rewriteLastShortToLong(self, value: int) -> None: bytes_written = self.f.write(struct.pack(self.longFmt, value)) self._verify_bytes_written(bytes_written, 4) + def _rewriteLast(self, value: int, field_size: int) -> None: + self.f.seek(-field_size, os.SEEK_CUR) + bytes_written = self.f.write( + struct.pack(self.endian + self._fmt(field_size), value) + ) + self._verify_bytes_written(bytes_written, field_size) + def rewriteLastShort(self, value: int) -> None: - self.f.seek(-2, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - self._verify_bytes_written(bytes_written, 2) + return self._rewriteLast(value, 2) def rewriteLastLong(self, value: int) -> None: - self.f.seek(-4, os.SEEK_CUR) - bytes_written = self.f.write(struct.pack(self.longFmt, value)) - self._verify_bytes_written(bytes_written, 4) + return self._rewriteLast(value, 4) def writeShort(self, value: int) -> None: bytes_written = self.f.write(struct.pack(self.shortFmt, value)) @@ -2180,32 +2194,22 @@ def fixIFD(self) -> None: cur_pos = self.f.tell() if is_local: - self.fixOffsets( - count, isShort=(field_size == 2), isLong=(field_size == 4) - ) + self._fixOffsets(count, field_size) self.f.seek(cur_pos + 4) else: self.f.seek(offset) - self.fixOffsets( - count, isShort=(field_size == 2), isLong=(field_size == 4) - ) + self._fixOffsets(count, field_size) self.f.seek(cur_pos) elif is_local: # skip the locally stored value that is not an offset self.f.seek(4, os.SEEK_CUR) - def fixOffsets( - self, count: int, isShort: bool = False, isLong: bool = False - ) -> None: - if not isShort and not isLong: - msg = "offset is neither short nor long" - raise RuntimeError(msg) - + def _fixOffsets(self, count: int, field_size: int) -> None: for i in range(count): - offset = self.readShort() if isShort else self.readLong() + offset = self._read(field_size) offset += self.offsetOfNewPage - if isShort and offset >= 65536: + if field_size == 2 and offset >= 65536: # offset is now too large - we must convert shorts to longs if count != 1: msg = "not implemented" @@ -2217,10 +2221,19 @@ def fixOffsets( self.f.seek(-10, os.SEEK_CUR) self.writeShort(TiffTags.LONG) # rewrite the type to LONG self.f.seek(8, os.SEEK_CUR) - elif isShort: - self.rewriteLastShort(offset) else: - self.rewriteLastLong(offset) + self._rewriteLast(offset, field_size) + + def fixOffsets( + self, count: int, isShort: bool = False, isLong: bool = False + ) -> None: + if isShort: + field_size = 2 + elif isLong: + field_size = 4 + else: + field_size = 0 + return self._fixOffsets(count, field_size) def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: