Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support writing LONG8 offsets in AppendingTiffWriter #8417

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Tests/test_file_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
69 changes: 41 additions & 28 deletions src/PIL/TiffImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2121,13 +2121,24 @@
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:
Expand All @@ -2140,15 +2151,18 @@
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)

Check warning on line 2162 in src/PIL/TiffImagePlugin.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/TiffImagePlugin.py#L2162

Added line #L2162 was not covered by tests

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))
Expand Down Expand Up @@ -2180,32 +2194,22 @@
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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should probably now also be a check for field_size == 4 and offset >= 2**32.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is conceivable that some users might not wish for us to silently upgrade tags to the LONG8 type, since it would be changing the saved image to rely on the BigTIFF specification.

# offset is now too large - we must convert shorts to longs
if count != 1:
msg = "not implemented"
Expand All @@ -2217,10 +2221,19 @@
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:
Expand Down
Loading