mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-02-14 18:40:53 +03:00
Allow saving multiple frames as BigTIFF
This commit is contained in:
parent
128f3f46d4
commit
618339e2d2
|
@ -117,10 +117,16 @@ class TestFileTiff:
|
||||||
|
|
||||||
def test_bigtiff_save(self, tmp_path: Path) -> None:
|
def test_bigtiff_save(self, tmp_path: Path) -> None:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = str(tmp_path / "temp.tif")
|
||||||
hopper().save(outfile, big_tiff=True)
|
im = hopper()
|
||||||
|
im.save(outfile, big_tiff=True)
|
||||||
|
|
||||||
with Image.open(outfile) as im:
|
with Image.open(outfile) as reloaded:
|
||||||
assert im.tag_v2._bigtiff is True
|
assert reloaded.tag_v2._bigtiff is True
|
||||||
|
|
||||||
|
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
|
||||||
|
|
||||||
|
with Image.open(outfile) as reloaded:
|
||||||
|
assert reloaded.tag_v2._bigtiff is True
|
||||||
|
|
||||||
def test_seek_too_large(self) -> None:
|
def test_seek_too_large(self) -> None:
|
||||||
with pytest.raises(ValueError, match="Unable to seek to frame"):
|
with pytest.raises(ValueError, match="Unable to seek to frame"):
|
||||||
|
@ -753,6 +759,13 @@ class TestFileTiff:
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
a.fixOffsets(1)
|
a.fixOffsets(1)
|
||||||
|
|
||||||
|
def test_appending_tiff_writer_writelong(self) -> None:
|
||||||
|
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||||
|
b = BytesIO(data)
|
||||||
|
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||||
|
a.writeLong(2**32 - 1)
|
||||||
|
assert b.getvalue() == data + b"\xff\xff\xff\xff"
|
||||||
|
|
||||||
def test_saving_icc_profile(self, tmp_path: Path) -> None:
|
def test_saving_icc_profile(self, tmp_path: Path) -> None:
|
||||||
# Tests saving TIFF with icc_profile set.
|
# Tests saving TIFF with icc_profile set.
|
||||||
# At the time of writing this will only work for non-compressed tiffs
|
# At the time of writing this will only work for non-compressed tiffs
|
||||||
|
|
|
@ -962,13 +962,16 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
||||||
result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
|
result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
|
||||||
|
|
||||||
entries: list[tuple[int, int, int, bytes, bytes]] = []
|
entries: list[tuple[int, int, int, bytes, bytes]] = []
|
||||||
offset += len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + 4
|
|
||||||
|
fmt = "Q" if self._bigtiff else "L"
|
||||||
|
fmt_size = 8 if self._bigtiff else 4
|
||||||
|
offset += (
|
||||||
|
len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size
|
||||||
|
)
|
||||||
stripoffsets = None
|
stripoffsets = None
|
||||||
|
|
||||||
# pass 1: convert tags to binary format
|
# pass 1: convert tags to binary format
|
||||||
# always write tags in ascending order
|
# always write tags in ascending order
|
||||||
fmt = "Q" if self._bigtiff else "L"
|
|
||||||
fmt_size = 8 if self._bigtiff else 4
|
|
||||||
for tag, value in sorted(self._tags_v2.items()):
|
for tag, value in sorted(self._tags_v2.items()):
|
||||||
if tag == STRIPOFFSETS:
|
if tag == STRIPOFFSETS:
|
||||||
stripoffsets = len(entries)
|
stripoffsets = len(entries)
|
||||||
|
@ -1024,7 +1027,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
|
||||||
)
|
)
|
||||||
|
|
||||||
# -- overwrite here for multi-page --
|
# -- overwrite here for multi-page --
|
||||||
result += b"\0\0\0\0" # end of entries
|
result += self._pack(fmt, 0) # end of entries
|
||||||
|
|
||||||
# pass 3: write auxiliary data to file
|
# pass 3: write auxiliary data to file
|
||||||
for tag, typ, count, value, data in entries:
|
for tag, typ, count, value, data in entries:
|
||||||
|
@ -2043,20 +2046,21 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
self.offsetOfNewPage = 0
|
self.offsetOfNewPage = 0
|
||||||
|
|
||||||
self.IIMM = iimm = self.f.read(4)
|
self.IIMM = iimm = self.f.read(4)
|
||||||
|
self._bigtiff = b"\x2B" in iimm
|
||||||
if not iimm:
|
if not iimm:
|
||||||
# empty file - first page
|
# empty file - first page
|
||||||
self.isFirst = True
|
self.isFirst = True
|
||||||
return
|
return
|
||||||
|
|
||||||
self.isFirst = False
|
self.isFirst = False
|
||||||
if iimm == b"II\x2a\x00":
|
if iimm not in PREFIXES:
|
||||||
self.setEndian("<")
|
|
||||||
elif iimm == b"MM\x00\x2a":
|
|
||||||
self.setEndian(">")
|
|
||||||
else:
|
|
||||||
msg = "Invalid TIFF file header"
|
msg = "Invalid TIFF file header"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
self.setEndian("<" if iimm.startswith(II) else ">")
|
||||||
|
|
||||||
|
if self._bigtiff:
|
||||||
|
self.f.seek(4, os.SEEK_CUR)
|
||||||
self.skipIFDs()
|
self.skipIFDs()
|
||||||
self.goToEnd()
|
self.goToEnd()
|
||||||
|
|
||||||
|
@ -2076,11 +2080,13 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
msg = "IIMM of new page doesn't match IIMM of first page"
|
msg = "IIMM of new page doesn't match IIMM of first page"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
ifd_offset = self.readLong()
|
if self._bigtiff:
|
||||||
|
self.f.seek(4, os.SEEK_CUR)
|
||||||
|
ifd_offset = self._read(8 if self._bigtiff else 4)
|
||||||
ifd_offset += self.offsetOfNewPage
|
ifd_offset += self.offsetOfNewPage
|
||||||
assert self.whereToWriteNewIFDOffset is not None
|
assert self.whereToWriteNewIFDOffset is not None
|
||||||
self.f.seek(self.whereToWriteNewIFDOffset)
|
self.f.seek(self.whereToWriteNewIFDOffset)
|
||||||
self.writeLong(ifd_offset)
|
self._write(ifd_offset, 8 if self._bigtiff else 4)
|
||||||
self.f.seek(ifd_offset)
|
self.f.seek(ifd_offset)
|
||||||
self.fixIFD()
|
self.fixIFD()
|
||||||
|
|
||||||
|
@ -2126,18 +2132,20 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
self.endian = endian
|
self.endian = endian
|
||||||
self.longFmt = f"{self.endian}L"
|
self.longFmt = f"{self.endian}L"
|
||||||
self.shortFmt = f"{self.endian}H"
|
self.shortFmt = f"{self.endian}H"
|
||||||
self.tagFormat = f"{self.endian}HHL"
|
self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L")
|
||||||
|
|
||||||
def skipIFDs(self) -> None:
|
def skipIFDs(self) -> None:
|
||||||
while True:
|
while True:
|
||||||
ifd_offset = self.readLong()
|
ifd_offset = self._read(8 if self._bigtiff else 4)
|
||||||
if ifd_offset == 0:
|
if ifd_offset == 0:
|
||||||
self.whereToWriteNewIFDOffset = self.f.tell() - 4
|
self.whereToWriteNewIFDOffset = self.f.tell() - (
|
||||||
|
8 if self._bigtiff else 4
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
self.f.seek(ifd_offset)
|
self.f.seek(ifd_offset)
|
||||||
num_tags = self.readShort()
|
num_tags = self._read(8 if self._bigtiff else 2)
|
||||||
self.f.seek(num_tags * 12, os.SEEK_CUR)
|
self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR)
|
||||||
|
|
||||||
def write(self, data: Buffer, /) -> int:
|
def write(self, data: Buffer, /) -> int:
|
||||||
return self.f.write(data)
|
return self.f.write(data)
|
||||||
|
@ -2185,13 +2193,17 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
def rewriteLastLong(self, value: int) -> None:
|
def rewriteLastLong(self, value: int) -> None:
|
||||||
return self._rewriteLast(value, 4)
|
return self._rewriteLast(value, 4)
|
||||||
|
|
||||||
|
def _write(self, value: int, field_size: int) -> None:
|
||||||
|
bytes_written = self.f.write(
|
||||||
|
struct.pack(self.endian + self._fmt(field_size), value)
|
||||||
|
)
|
||||||
|
self._verify_bytes_written(bytes_written, field_size)
|
||||||
|
|
||||||
def writeShort(self, value: int) -> None:
|
def writeShort(self, value: int) -> None:
|
||||||
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
|
self._write(value, 2)
|
||||||
self._verify_bytes_written(bytes_written, 2)
|
|
||||||
|
|
||||||
def writeLong(self, value: int) -> None:
|
def writeLong(self, value: int) -> None:
|
||||||
bytes_written = self.f.write(struct.pack(self.longFmt, value))
|
self._write(value, 4)
|
||||||
self._verify_bytes_written(bytes_written, 4)
|
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
self.finalize()
|
self.finalize()
|
||||||
|
@ -2199,24 +2211,27 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
self.f.close()
|
self.f.close()
|
||||||
|
|
||||||
def fixIFD(self) -> None:
|
def fixIFD(self) -> None:
|
||||||
num_tags = self.readShort()
|
num_tags = self._read(8 if self._bigtiff else 2)
|
||||||
|
|
||||||
for i in range(num_tags):
|
for i in range(num_tags):
|
||||||
tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8))
|
tag, field_type, count = struct.unpack(
|
||||||
|
self.tagFormat, self.f.read(12 if self._bigtiff else 8)
|
||||||
|
)
|
||||||
|
|
||||||
field_size = self.fieldSizes[field_type]
|
field_size = self.fieldSizes[field_type]
|
||||||
total_size = field_size * count
|
total_size = field_size * count
|
||||||
is_local = total_size <= 4
|
fmt_size = 8 if self._bigtiff else 4
|
||||||
|
is_local = total_size <= fmt_size
|
||||||
if not is_local:
|
if not is_local:
|
||||||
offset = self.readLong() + self.offsetOfNewPage
|
offset = self._read(fmt_size) + self.offsetOfNewPage
|
||||||
self.rewriteLastLong(offset)
|
self._rewriteLast(offset, fmt_size)
|
||||||
|
|
||||||
if tag in self.Tags:
|
if tag in self.Tags:
|
||||||
cur_pos = self.f.tell()
|
cur_pos = self.f.tell()
|
||||||
|
|
||||||
if is_local:
|
if is_local:
|
||||||
self._fixOffsets(count, field_size)
|
self._fixOffsets(count, field_size)
|
||||||
self.f.seek(cur_pos + 4)
|
self.f.seek(cur_pos + fmt_size)
|
||||||
else:
|
else:
|
||||||
self.f.seek(offset)
|
self.f.seek(offset)
|
||||||
self._fixOffsets(count, field_size)
|
self._fixOffsets(count, field_size)
|
||||||
|
@ -2224,7 +2239,7 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
|
|
||||||
elif is_local:
|
elif is_local:
|
||||||
# skip the locally stored value that is not an offset
|
# skip the locally stored value that is not an offset
|
||||||
self.f.seek(4, os.SEEK_CUR)
|
self.f.seek(fmt_size, os.SEEK_CUR)
|
||||||
|
|
||||||
def _fixOffsets(self, count: int, field_size: int) -> None:
|
def _fixOffsets(self, count: int, field_size: int) -> None:
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user