mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-11-04 09:57:43 +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