Skip to content

Commit af3b904

Browse files
authored
Merge pull request #8663 from radarhere/bigtiff
2 parents 93f45a3 + a8381c6 commit af3b904

File tree

2 files changed

+108
-45
lines changed

2 files changed

+108
-45
lines changed

Tests/test_file_tiff.py

Lines changed: 41 additions & 4 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"):
@@ -740,7 +746,7 @@ def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
740746
assert reread.n_frames == 3
741747

742748
def test_fixoffsets(self) -> None:
743-
b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00")
749+
b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
744750
with TiffImagePlugin.AppendingTiffWriter(b) as a:
745751
b.seek(0)
746752
a.fixOffsets(1, isShort=True)
@@ -753,6 +759,37 @@ def test_fixoffsets(self) -> None:
753759
with pytest.raises(RuntimeError):
754760
a.fixOffsets(1)
755761

762+
b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
763+
with TiffImagePlugin.AppendingTiffWriter(b) as a:
764+
a.offsetOfNewPage = 2**16
765+
766+
b.seek(0)
767+
a.fixOffsets(1, isShort=True)
768+
769+
b = BytesIO(b"II\x2B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
770+
with TiffImagePlugin.AppendingTiffWriter(b) as a:
771+
a.offsetOfNewPage = 2**32
772+
773+
b.seek(0)
774+
a.fixOffsets(1, isShort=True)
775+
776+
b.seek(0)
777+
a.fixOffsets(1, isLong=True)
778+
779+
def test_appending_tiff_writer_writelong(self) -> None:
780+
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
781+
b = BytesIO(data)
782+
with TiffImagePlugin.AppendingTiffWriter(b) as a:
783+
a.writeLong(2**32 - 1)
784+
assert b.getvalue() == data + b"\xff\xff\xff\xff"
785+
786+
def test_appending_tiff_writer_rewritelastshorttolong(self) -> None:
787+
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
788+
b = BytesIO(data)
789+
with TiffImagePlugin.AppendingTiffWriter(b) as a:
790+
a.rewriteLastShortToLong(2**32 - 1)
791+
assert b.getvalue() == data[:-2] + b"\xff\xff\xff\xff"
792+
756793
def test_saving_icc_profile(self, tmp_path: Path) -> None:
757794
# Tests saving TIFF with icc_profile set.
758795
# At the time of writing this will only work for non-compressed tiffs

src/PIL/TiffImagePlugin.py

Lines changed: 67 additions & 41 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:
@@ -2044,20 +2047,21 @@ def setup(self) -> None:
20442047
self.offsetOfNewPage = 0
20452048

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

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

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

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

2080-
ifd_offset = self.readLong()
2084+
if self._bigtiff:
2085+
self.f.seek(4, os.SEEK_CUR)
2086+
ifd_offset = self._read(8 if self._bigtiff else 4)
20812087
ifd_offset += self.offsetOfNewPage
20822088
assert self.whereToWriteNewIFDOffset is not None
20832089
self.f.seek(self.whereToWriteNewIFDOffset)
2084-
self.writeLong(ifd_offset)
2090+
self._write(ifd_offset, 8 if self._bigtiff else 4)
20852091
self.f.seek(ifd_offset)
20862092
self.fixIFD()
20872093

@@ -2127,18 +2133,20 @@ def setEndian(self, endian: str) -> None:
21272133
self.endian = endian
21282134
self.longFmt = f"{self.endian}L"
21292135
self.shortFmt = f"{self.endian}H"
2130-
self.tagFormat = f"{self.endian}HHL"
2136+
self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L")
21312137

21322138
def skipIFDs(self) -> None:
21332139
while True:
2134-
ifd_offset = self.readLong()
2140+
ifd_offset = self._read(8 if self._bigtiff else 4)
21352141
if ifd_offset == 0:
2136-
self.whereToWriteNewIFDOffset = self.f.tell() - 4
2142+
self.whereToWriteNewIFDOffset = self.f.tell() - (
2143+
8 if self._bigtiff else 4
2144+
)
21372145
break
21382146

21392147
self.f.seek(ifd_offset)
2140-
num_tags = self.readShort()
2141-
self.f.seek(num_tags * 12, os.SEEK_CUR)
2148+
num_tags = self._read(8 if self._bigtiff else 2)
2149+
self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR)
21422150

21432151
def write(self, data: Buffer, /) -> int:
21442152
return self.f.write(data)
@@ -2168,81 +2176,99 @@ def _verify_bytes_written(bytes_written: int | None, expected: int) -> None:
21682176
msg = f"wrote only {bytes_written} bytes but wanted {expected}"
21692177
raise RuntimeError(msg)
21702178

2171-
def rewriteLastShortToLong(self, value: int) -> None:
2172-
self.f.seek(-2, os.SEEK_CUR)
2173-
bytes_written = self.f.write(struct.pack(self.longFmt, value))
2174-
self._verify_bytes_written(bytes_written, 4)
2175-
2176-
def _rewriteLast(self, value: int, field_size: int) -> None:
2179+
def _rewriteLast(
2180+
self, value: int, field_size: int, new_field_size: int = 0
2181+
) -> None:
21772182
self.f.seek(-field_size, os.SEEK_CUR)
2183+
if not new_field_size:
2184+
new_field_size = field_size
21782185
bytes_written = self.f.write(
2179-
struct.pack(self.endian + self._fmt(field_size), value)
2186+
struct.pack(self.endian + self._fmt(new_field_size), value)
21802187
)
2181-
self._verify_bytes_written(bytes_written, field_size)
2188+
self._verify_bytes_written(bytes_written, new_field_size)
2189+
2190+
def rewriteLastShortToLong(self, value: int) -> None:
2191+
self._rewriteLast(value, 2, 4)
21822192

21832193
def rewriteLastShort(self, value: int) -> None:
21842194
return self._rewriteLast(value, 2)
21852195

21862196
def rewriteLastLong(self, value: int) -> None:
21872197
return self._rewriteLast(value, 4)
21882198

2199+
def _write(self, value: int, field_size: int) -> None:
2200+
bytes_written = self.f.write(
2201+
struct.pack(self.endian + self._fmt(field_size), value)
2202+
)
2203+
self._verify_bytes_written(bytes_written, field_size)
2204+
21892205
def writeShort(self, value: int) -> None:
2190-
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
2191-
self._verify_bytes_written(bytes_written, 2)
2206+
self._write(value, 2)
21922207

21932208
def writeLong(self, value: int) -> None:
2194-
bytes_written = self.f.write(struct.pack(self.longFmt, value))
2195-
self._verify_bytes_written(bytes_written, 4)
2209+
self._write(value, 4)
21962210

21972211
def close(self) -> None:
21982212
self.finalize()
21992213
if self.close_fp:
22002214
self.f.close()
22012215

22022216
def fixIFD(self) -> None:
2203-
num_tags = self.readShort()
2217+
num_tags = self._read(8 if self._bigtiff else 2)
22042218

22052219
for i in range(num_tags):
2206-
tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8))
2220+
tag, field_type, count = struct.unpack(
2221+
self.tagFormat, self.f.read(12 if self._bigtiff else 8)
2222+
)
22072223

22082224
field_size = self.fieldSizes[field_type]
22092225
total_size = field_size * count
2210-
is_local = total_size <= 4
2226+
fmt_size = 8 if self._bigtiff else 4
2227+
is_local = total_size <= fmt_size
22112228
if not is_local:
2212-
offset = self.readLong() + self.offsetOfNewPage
2213-
self.rewriteLastLong(offset)
2229+
offset = self._read(fmt_size) + self.offsetOfNewPage
2230+
self._rewriteLast(offset, fmt_size)
22142231

22152232
if tag in self.Tags:
22162233
cur_pos = self.f.tell()
22172234

22182235
if is_local:
22192236
self._fixOffsets(count, field_size)
2220-
self.f.seek(cur_pos + 4)
2237+
self.f.seek(cur_pos + fmt_size)
22212238
else:
22222239
self.f.seek(offset)
22232240
self._fixOffsets(count, field_size)
22242241
self.f.seek(cur_pos)
22252242

22262243
elif is_local:
22272244
# skip the locally stored value that is not an offset
2228-
self.f.seek(4, os.SEEK_CUR)
2245+
self.f.seek(fmt_size, os.SEEK_CUR)
22292246

22302247
def _fixOffsets(self, count: int, field_size: int) -> None:
22312248
for i in range(count):
22322249
offset = self._read(field_size)
22332250
offset += self.offsetOfNewPage
2234-
if field_size == 2 and offset >= 65536:
2235-
# offset is now too large - we must convert shorts to longs
2251+
2252+
new_field_size = 0
2253+
if self._bigtiff and field_size in (2, 4) and offset >= 2**32:
2254+
# offset is now too large - we must convert long to long8
2255+
new_field_size = 8
2256+
elif field_size == 2 and offset >= 2**16:
2257+
# offset is now too large - we must convert short to long
2258+
new_field_size = 4
2259+
if new_field_size:
22362260
if count != 1:
22372261
msg = "not implemented"
22382262
raise RuntimeError(msg) # XXX TODO
22392263

22402264
# simple case - the offset is just one and therefore it is
22412265
# local (not referenced with another offset)
2242-
self.rewriteLastShortToLong(offset)
2243-
self.f.seek(-10, os.SEEK_CUR)
2244-
self.writeShort(TiffTags.LONG) # rewrite the type to LONG
2245-
self.f.seek(8, os.SEEK_CUR)
2266+
self._rewriteLast(offset, field_size, new_field_size)
2267+
# Move back past the new offset, past 'count', and before 'field_type'
2268+
rewind = -new_field_size - 4 - 2
2269+
self.f.seek(rewind, os.SEEK_CUR)
2270+
self.writeShort(new_field_size) # rewrite the type
2271+
self.f.seek(2 - rewind, os.SEEK_CUR)
22462272
else:
22472273
self._rewriteLast(offset, field_size)
22482274

0 commit comments

Comments
 (0)