Skip to content

Commit 618339e

Browse files
committed
Allow saving multiple frames as BigTIFF
1 parent 128f3f4 commit 618339e

2 files changed

Lines changed: 58 additions & 30 deletions

File tree

Tests/test_file_tiff.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,16 @@ def test_bigtiff(self, tmp_path: Path) -> None:
117117

118118
def test_bigtiff_save(self, tmp_path: Path) -> None:
119119
outfile = str(tmp_path / "temp.tif")
120-
hopper().save(outfile, big_tiff=True)
120+
im = hopper()
121+
im.save(outfile, big_tiff=True)
121122

122-
with Image.open(outfile) as im:
123-
assert im.tag_v2._bigtiff is True
123+
with Image.open(outfile) as reloaded:
124+
assert reloaded.tag_v2._bigtiff is True
125+
126+
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
127+
128+
with Image.open(outfile) as reloaded:
129+
assert reloaded.tag_v2._bigtiff is True
124130

125131
def test_seek_too_large(self) -> None:
126132
with pytest.raises(ValueError, match="Unable to seek to frame"):
@@ -753,6 +759,13 @@ def test_fixoffsets(self) -> None:
753759
with pytest.raises(RuntimeError):
754760
a.fixOffsets(1)
755761

762+
def test_appending_tiff_writer_writelong(self) -> None:
763+
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
764+
b = BytesIO(data)
765+
with TiffImagePlugin.AppendingTiffWriter(b) as a:
766+
a.writeLong(2**32 - 1)
767+
assert b.getvalue() == data + b"\xff\xff\xff\xff"
768+
756769
def test_saving_icc_profile(self, tmp_path: Path) -> None:
757770
# Tests saving TIFF with icc_profile set.
758771
# At the time of writing this will only work for non-compressed tiffs

src/PIL/TiffImagePlugin.py

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -962,13 +962,16 @@ def tobytes(self, offset: int = 0) -> bytes:
962962
result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
963963

964964
entries: list[tuple[int, int, int, bytes, bytes]] = []
965-
offset += len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + 4
965+
966+
fmt = "Q" if self._bigtiff else "L"
967+
fmt_size = 8 if self._bigtiff else 4
968+
offset += (
969+
len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size
970+
)
966971
stripoffsets = None
967972

968973
# pass 1: convert tags to binary format
969974
# always write tags in ascending order
970-
fmt = "Q" if self._bigtiff else "L"
971-
fmt_size = 8 if self._bigtiff else 4
972975
for tag, value in sorted(self._tags_v2.items()):
973976
if tag == STRIPOFFSETS:
974977
stripoffsets = len(entries)
@@ -1024,7 +1027,7 @@ def tobytes(self, offset: int = 0) -> bytes:
10241027
)
10251028

10261029
# -- overwrite here for multi-page --
1027-
result += b"\0\0\0\0" # end of entries
1030+
result += self._pack(fmt, 0) # end of entries
10281031

10291032
# pass 3: write auxiliary data to file
10301033
for tag, typ, count, value, data in entries:
@@ -2043,20 +2046,21 @@ def setup(self) -> None:
20432046
self.offsetOfNewPage = 0
20442047

20452048
self.IIMM = iimm = self.f.read(4)
2049+
self._bigtiff = b"\x2B" in iimm
20462050
if not iimm:
20472051
# empty file - first page
20482052
self.isFirst = True
20492053
return
20502054

20512055
self.isFirst = False
2052-
if iimm == b"II\x2a\x00":
2053-
self.setEndian("<")
2054-
elif iimm == b"MM\x00\x2a":
2055-
self.setEndian(">")
2056-
else:
2056+
if iimm not in PREFIXES:
20572057
msg = "Invalid TIFF file header"
20582058
raise RuntimeError(msg)
20592059

2060+
self.setEndian("<" if iimm.startswith(II) else ">")
2061+
2062+
if self._bigtiff:
2063+
self.f.seek(4, os.SEEK_CUR)
20602064
self.skipIFDs()
20612065
self.goToEnd()
20622066

@@ -2076,11 +2080,13 @@ def finalize(self) -> None:
20762080
msg = "IIMM of new page doesn't match IIMM of first page"
20772081
raise RuntimeError(msg)
20782082

2079-
ifd_offset = self.readLong()
2083+
if self._bigtiff:
2084+
self.f.seek(4, os.SEEK_CUR)
2085+
ifd_offset = self._read(8 if self._bigtiff else 4)
20802086
ifd_offset += self.offsetOfNewPage
20812087
assert self.whereToWriteNewIFDOffset is not None
20822088
self.f.seek(self.whereToWriteNewIFDOffset)
2083-
self.writeLong(ifd_offset)
2089+
self._write(ifd_offset, 8 if self._bigtiff else 4)
20842090
self.f.seek(ifd_offset)
20852091
self.fixIFD()
20862092

@@ -2126,18 +2132,20 @@ def setEndian(self, endian: str) -> None:
21262132
self.endian = endian
21272133
self.longFmt = f"{self.endian}L"
21282134
self.shortFmt = f"{self.endian}H"
2129-
self.tagFormat = f"{self.endian}HHL"
2135+
self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L")
21302136

21312137
def skipIFDs(self) -> None:
21322138
while True:
2133-
ifd_offset = self.readLong()
2139+
ifd_offset = self._read(8 if self._bigtiff else 4)
21342140
if ifd_offset == 0:
2135-
self.whereToWriteNewIFDOffset = self.f.tell() - 4
2141+
self.whereToWriteNewIFDOffset = self.f.tell() - (
2142+
8 if self._bigtiff else 4
2143+
)
21362144
break
21372145

21382146
self.f.seek(ifd_offset)
2139-
num_tags = self.readShort()
2140-
self.f.seek(num_tags * 12, os.SEEK_CUR)
2147+
num_tags = self._read(8 if self._bigtiff else 2)
2148+
self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR)
21412149

21422150
def write(self, data: Buffer, /) -> int:
21432151
return self.f.write(data)
@@ -2185,46 +2193,53 @@ def rewriteLastShort(self, value: int) -> None:
21852193
def rewriteLastLong(self, value: int) -> None:
21862194
return self._rewriteLast(value, 4)
21872195

2196+
def _write(self, value: int, field_size: int) -> None:
2197+
bytes_written = self.f.write(
2198+
struct.pack(self.endian + self._fmt(field_size), value)
2199+
)
2200+
self._verify_bytes_written(bytes_written, field_size)
2201+
21882202
def writeShort(self, value: int) -> None:
2189-
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
2190-
self._verify_bytes_written(bytes_written, 2)
2203+
self._write(value, 2)
21912204

21922205
def writeLong(self, value: int) -> None:
2193-
bytes_written = self.f.write(struct.pack(self.longFmt, value))
2194-
self._verify_bytes_written(bytes_written, 4)
2206+
self._write(value, 4)
21952207

21962208
def close(self) -> None:
21972209
self.finalize()
21982210
if self.close_fp:
21992211
self.f.close()
22002212

22012213
def fixIFD(self) -> None:
2202-
num_tags = self.readShort()
2214+
num_tags = self._read(8 if self._bigtiff else 2)
22032215

22042216
for i in range(num_tags):
2205-
tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8))
2217+
tag, field_type, count = struct.unpack(
2218+
self.tagFormat, self.f.read(12 if self._bigtiff else 8)
2219+
)
22062220

22072221
field_size = self.fieldSizes[field_type]
22082222
total_size = field_size * count
2209-
is_local = total_size <= 4
2223+
fmt_size = 8 if self._bigtiff else 4
2224+
is_local = total_size <= fmt_size
22102225
if not is_local:
2211-
offset = self.readLong() + self.offsetOfNewPage
2212-
self.rewriteLastLong(offset)
2226+
offset = self._read(fmt_size) + self.offsetOfNewPage
2227+
self._rewriteLast(offset, fmt_size)
22132228

22142229
if tag in self.Tags:
22152230
cur_pos = self.f.tell()
22162231

22172232
if is_local:
22182233
self._fixOffsets(count, field_size)
2219-
self.f.seek(cur_pos + 4)
2234+
self.f.seek(cur_pos + fmt_size)
22202235
else:
22212236
self.f.seek(offset)
22222237
self._fixOffsets(count, field_size)
22232238
self.f.seek(cur_pos)
22242239

22252240
elif is_local:
22262241
# skip the locally stored value that is not an offset
2227-
self.f.seek(4, os.SEEK_CUR)
2242+
self.f.seek(fmt_size, os.SEEK_CUR)
22282243

22292244
def _fixOffsets(self, count: int, field_size: int) -> None:
22302245
for i in range(count):

0 commit comments

Comments
 (0)