Skip to content

Commit c7026d9

Browse files
authored
Merge pull request #8642 from radarhere/bigtiff
2 parents c3fac1d + 1de617f commit c7026d9

File tree

4 files changed

+46
-36
lines changed

4 files changed

+46
-36
lines changed

Tests/test_file_tiff.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ def test_bigtiff(self, tmp_path: Path) -> None:
115115
outfile = str(tmp_path / "temp.tif")
116116
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
117117

118+
def test_bigtiff_save(self, tmp_path: Path) -> None:
119+
outfile = str(tmp_path / "temp.tif")
120+
hopper().save(outfile, big_tiff=True)
121+
122+
with Image.open(outfile) as im:
123+
assert im.tag_v2._bigtiff is True
124+
118125
def test_seek_too_large(self) -> None:
119126
with pytest.raises(ValueError, match="Unable to seek to frame"):
120127
Image.open("Tests/images/seek_too_large.tif")

docs/handbook/image-file-formats.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,6 +1208,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
12081208

12091209
.. versionadded:: 8.4.0
12101210

1211+
**big_tiff**
1212+
If true, the image will be saved as a BigTIFF.
1213+
1214+
.. versionadded:: 11.1.0
1215+
12111216
**compression**
12121217
A string containing the desired compression method for the
12131218
file. (valid only with libtiff installed) Valid compression

docs/releasenotes/11.1.0.rst

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,6 @@
11
11.1.0
22
------
33

4-
Security
5-
========
6-
7-
TODO
8-
^^^^
9-
10-
TODO
11-
12-
:cve:`YYYY-XXXXX`: TODO
13-
^^^^^^^^^^^^^^^^^^^^^^^
14-
15-
TODO
16-
17-
Backwards Incompatible Changes
18-
==============================
19-
20-
TODO
21-
^^^^
22-
234
Deprecations
245
============
256

@@ -66,6 +47,13 @@ zlib library, and what version of zlib-ng is being used::
6647
features.check_feature("zlib_ng") # True or False
6748
features.version_feature("zlib_ng") # "2.2.2" for example, or None
6849

50+
Saving TIFF as BigTIFF
51+
^^^^^^^^^^^^^^^^^^^^^^
52+
53+
TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument::
54+
55+
im.save("out.tiff", big_tiff=True)
56+
6957
Other Changes
7058
=============
7159

src/PIL/TiffImagePlugin.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
582582

583583
def __init__(
584584
self,
585-
ifh: bytes = b"II\052\0\0\0\0\0",
585+
ifh: bytes = b"II\x2A\x00\x00\x00\x00\x00",
586586
prefix: bytes | None = None,
587587
group: int | None = None,
588588
) -> None:
@@ -949,28 +949,34 @@ def load(self, fp: IO[bytes]) -> None:
949949
warnings.warn(str(msg))
950950
return
951951

952+
def _get_ifh(self):
953+
ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42)
954+
if self._bigtiff:
955+
ifh += self._pack("HH", 8, 0)
956+
ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8)
957+
958+
return ifh
959+
952960
def tobytes(self, offset: int = 0) -> bytes:
953961
# FIXME What about tagdata?
954-
result = self._pack("H", len(self._tags_v2))
962+
result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
955963

956964
entries: list[tuple[int, int, int, bytes, bytes]] = []
957-
offset = offset + len(result) + len(self._tags_v2) * 12 + 4
965+
offset += len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + 4
958966
stripoffsets = None
959967

960968
# pass 1: convert tags to binary format
961969
# always write tags in ascending order
970+
fmt = "Q" if self._bigtiff else "L"
971+
fmt_size = 8 if self._bigtiff else 4
962972
for tag, value in sorted(self._tags_v2.items()):
963973
if tag == STRIPOFFSETS:
964974
stripoffsets = len(entries)
965975
typ = self.tagtype[tag]
966976
logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
967977
is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
968978
if is_ifd:
969-
if self._endian == "<":
970-
ifh = b"II\x2A\x00\x08\x00\x00\x00"
971-
else:
972-
ifh = b"MM\x00\x2A\x00\x00\x00\x08"
973-
ifd = ImageFileDirectory_v2(ifh, group=tag)
979+
ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag)
974980
values = self._tags_v2[tag]
975981
for ifd_tag, ifd_value in values.items():
976982
ifd[ifd_tag] = ifd_value
@@ -993,10 +999,10 @@ def tobytes(self, offset: int = 0) -> bytes:
993999
else:
9941000
count = len(values)
9951001
# figure out if data fits into the entry
996-
if len(data) <= 4:
997-
entries.append((tag, typ, count, data.ljust(4, b"\0"), b""))
1002+
if len(data) <= fmt_size:
1003+
entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b""))
9981004
else:
999-
entries.append((tag, typ, count, self._pack("L", offset), data))
1005+
entries.append((tag, typ, count, self._pack(fmt, offset), data))
10001006
offset += (len(data) + 1) // 2 * 2 # pad to word
10011007

10021008
# update strip offset data to point beyond auxiliary data
@@ -1007,13 +1013,15 @@ def tobytes(self, offset: int = 0) -> bytes:
10071013
values = [val + offset for val in handler(self, data, self.legacy_api)]
10081014
data = self._write_dispatch[typ](self, *values)
10091015
else:
1010-
value = self._pack("L", self._unpack("L", value)[0] + offset)
1016+
value = self._pack(fmt, self._unpack(fmt, value)[0] + offset)
10111017
entries[stripoffsets] = tag, typ, count, value, data
10121018

10131019
# pass 2: write entries to file
10141020
for tag, typ, count, value, data in entries:
10151021
logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data))
1016-
result += self._pack("HHL4s", tag, typ, count, value)
1022+
result += self._pack(
1023+
"HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value
1024+
)
10171025

10181026
# -- overwrite here for multi-page --
10191027
result += b"\0\0\0\0" # end of entries
@@ -1028,8 +1036,7 @@ def tobytes(self, offset: int = 0) -> bytes:
10281036

10291037
def save(self, fp: IO[bytes]) -> int:
10301038
if fp.tell() == 0: # skip TIFF header on subsequent pages
1031-
# tiff header -- PIL always starts the first IFD at offset 8
1032-
fp.write(self._prefix + self._pack("HL", 42, 8))
1039+
fp.write(self._get_ifh())
10331040

10341041
offset = fp.tell()
10351042
result = self.tobytes(offset)
@@ -1680,10 +1687,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
16801687
msg = f"cannot write mode {im.mode} as TIFF"
16811688
raise OSError(msg) from e
16821689

1683-
ifd = ImageFileDirectory_v2(prefix=prefix)
1684-
16851690
encoderinfo = im.encoderinfo
16861691
encoderconfig = im.encoderconfig
1692+
1693+
ifd = ImageFileDirectory_v2(prefix=prefix)
1694+
if encoderinfo.get("big_tiff"):
1695+
ifd._bigtiff = True
1696+
16871697
try:
16881698
compression = encoderinfo["compression"]
16891699
except KeyError:

0 commit comments

Comments
 (0)