Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added BigTIFF reading #6097

Merged
merged 2 commits into from Mar 11, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added Tests/images/hopper_bigtiff.tif
Binary file not shown.
4 changes: 4 additions & 0 deletions Tests/test_file_tiff.py
Expand Up @@ -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",
[
Expand Down
13 changes: 10 additions & 3 deletions src/PIL/Image.py
Expand Up @@ -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


Expand Down Expand Up @@ -1416,6 +1416,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:
Expand Down Expand Up @@ -3423,6 +3424,7 @@ def _apply_env_variables(env=None):

class Exif(MutableMapping):
endian = None
bigtiff = False

def __init__(self):
self._data = {}
Expand Down Expand Up @@ -3458,10 +3460,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"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\x2B is 43, and \x2A is 42, as per https://www.awaresystems.be/imaging/tiff/bigtiff.html

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,
Expand Down
33 changes: 27 additions & 6 deletions src/PIL/TiffImagePlugin.py
Expand Up @@ -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
]


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://www.awaresystems.be/imaging/tiff/bigtiff.html

The StripOffsets, StripByteCounts, TileOffsets, and TileByteCounts tags are allowed to have the datatype TIFF_LONG8 in BigTIFF.

],
)
)
Expand Down Expand Up @@ -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")
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions src/PIL/TiffTags.py
Expand Up @@ -74,6 +74,7 @@ def lookup(tag, group=None):
FLOAT = 11
DOUBLE = 12
IFD = 13
LONG8 = 16

TAGS_V2 = {
254: ("NewSubfileType", LONG, 1),
Expand Down