diff --git a/Tests/images/hopper_bigtiff.tif b/Tests/images/hopper_bigtiff.tif new file mode 100644 index 00000000000..9588a37d80b Binary files /dev/null and b/Tests/images/hopper_bigtiff.tif differ diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 28aeff075cb..d8d5753f6db 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -87,6 +87,10 @@ def test_mac_tiff(self): assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) + def test_bigtiff(self): + with Image.open("Tests/images/hopper_bigtiff.tif") as im: + assert_image_equal_tofile(im, "Tests/images/hopper.tif") + @pytest.mark.parametrize( "file_name,mode,size,offset", [ diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 22b185e95ac..1e5203ac213 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -177,6 +177,11 @@ Image._repr_pretty_ identity of the object. This allows Jupyter to describe an image and have that description stay the same on subsequent executions of the same code. +Added BigTIFF reading +^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for reading BigTIFF images. + Added BLP saving ^^^^^^^^^^^^^^^^ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e174d374321..8213f79b1f0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -49,7 +49,7 @@ # PILLOW_VERSION was removed in Pillow 9.0.0. # Use __version__ instead. from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins -from ._binary import i32le +from ._binary import i32le, o32be, o32le from ._util import deferred_error, isPath @@ -1418,6 +1418,7 @@ def getexif(self): "".join(self.info["Raw profile type exif"].split("\n")[3:]) ) elif hasattr(self, "tag_v2"): + self._exif.bigtiff = self.tag_v2._bigtiff self._exif.endian = self.tag_v2._endian self._exif.load_from_fp(self.fp, self.tag_v2._offset) if exif_info is not None: @@ -3426,6 +3427,7 @@ def _apply_env_variables(env=None): class Exif(MutableMapping): endian = None + bigtiff = False def __init__(self): self._data = {} @@ -3461,10 +3463,15 @@ def _get_ifd_dict(self, offset): return self._fixup_dict(info) def _get_head(self): + version = b"\x2B" if self.bigtiff else b"\x2A" if self.endian == "<": - return b"II\x2A\x00\x08\x00\x00\x00" + head = b"II" + version + b"\x00" + o32le(8) else: - return b"MM\x00\x2A\x00\x00\x00\x08" + head = b"MM\x00" + version + o32be(8) + if self.bigtiff: + head += o32le(8) if self.endian == "<" else o32be(8) + head += b"\x00\x00\x00\x00" + return head def load(self, data): # Extract EXIF information. This is highly experimental, diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 1d9a4881bd9..3b70512605f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -260,6 +260,8 @@ b"II\x2A\x00", # Valid TIFF header with little-endian byte order b"MM\x2A\x00", # Invalid TIFF header, assume big-endian b"II\x00\x2A", # Invalid TIFF header, assume little-endian + b"MM\x00\x2B", # BigTIFF with big-endian byte order + b"II\x2B\x00", # BigTIFF with little-endian byte order ] @@ -502,11 +504,14 @@ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): self._endian = "<" else: raise SyntaxError("not a TIFF IFD") + self._bigtiff = ifh[2] == 43 self.group = group self.tagtype = {} """ Dictionary of tag types """ self.reset() - (self.next,) = self._unpack("L", ifh[4:]) + (self.next,) = ( + self._unpack("Q", ifh[8:]) if self._bigtiff else self._unpack("L", ifh[4:]) + ) self._legacy_api = False prefix = property(lambda self: self._prefix) @@ -699,6 +704,7 @@ def _register_basic(idx_fmt_name): (TiffTags.FLOAT, "f", "float"), (TiffTags.DOUBLE, "d", "double"), (TiffTags.IFD, "L", "long"), + (TiffTags.LONG8, "Q", "long8"), ], ) ) @@ -776,8 +782,17 @@ def load(self, fp): self._offset = fp.tell() try: - for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]): - tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12)) + tag_count = ( + self._unpack("Q", self._ensure_read(fp, 8)) + if self._bigtiff + else self._unpack("H", self._ensure_read(fp, 2)) + )[0] + for i in range(tag_count): + tag, typ, count, data = ( + self._unpack("HHQ8s", self._ensure_read(fp, 20)) + if self._bigtiff + else self._unpack("HHL4s", self._ensure_read(fp, 12)) + ) tagname = TiffTags.lookup(tag, self.group).name typname = TYPES.get(typ, "unknown") @@ -789,9 +804,9 @@ def load(self, fp): logger.debug(msg + f" - unsupported type {typ}") continue # ignore unsupported type size = count * unit_size - if size > 4: + if size > (8 if self._bigtiff else 4): here = fp.tell() - (offset,) = self._unpack("L", data) + (offset,) = self._unpack("Q" if self._bigtiff else "L", data) msg += f" Tag Location: {here} - Data Location: {offset}" fp.seek(offset) data = ImageFile._safe_read(fp, size) @@ -820,7 +835,11 @@ def load(self, fp): ) logger.debug(msg) - (self.next,) = self._unpack("L", self._ensure_read(fp, 4)) + (self.next,) = ( + self._unpack("Q", self._ensure_read(fp, 8)) + if self._bigtiff + else self._unpack("L", self._ensure_read(fp, 4)) + ) except OSError as msg: warnings.warn(str(msg)) return @@ -1042,6 +1061,8 @@ def _open(self): # Header ifh = self.fp.read(8) + if ifh[2] == 43: + ifh += self.fp.read(8) self.tag_v2 = ImageFileDirectory_v2(ifh) diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 88856aa92d5..b37c8cf5ff4 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -74,6 +74,7 @@ def lookup(tag, group=None): FLOAT = 11 DOUBLE = 12 IFD = 13 +LONG8 = 16 TAGS_V2 = { 254: ("NewSubfileType", LONG, 1),