Skip to content

Commit

Permalink
Merge pull request #8417 from radarhere/appendingTiffWriter
Browse files Browse the repository at this point in the history
Support writing LONG8 offsets in AppendingTiffWriter
  • Loading branch information
hugovk authored Oct 12, 2024
2 parents 3dded83 + 7cf351c commit fd74857
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 32 deletions.
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 @@ -2115,13 +2115,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:
Expand All @@ -2134,15 +2145,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))
Expand Down Expand Up @@ -2174,32 +2188,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"
Expand All @@ -2211,10 +2215,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:
Expand Down

0 comments on commit fd74857

Please sign in to comment.