From 622ba122cef01cc33abd58010fe7abe1e0e4175c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Apr 2021 19:46:49 +1000 Subject: [PATCH] Added Exif load_from_fp method to get TIFF tag_v2 data --- Tests/test_file_tiff.py | 44 +++++++++++++++++++++++++++++++++++ Tests/test_image.py | 21 +++++++++++++++++ src/PIL/Image.py | 51 ++++++++++++++++++++++++++++++++--------- 3 files changed, 105 insertions(+), 11 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index c24438c48..eef2d0578 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -389,6 +389,50 @@ class TestFileTiff: with Image.open("Tests/images/ifd_tag_type.tiff") as im: assert 0x8825 in im.tag_v2 + def test_exif(self): + with Image.open("Tests/images/ifd_tag_type.tiff") as im: + exif = im.getexif() + + assert sorted(exif.keys()) == [ + 256, + 257, + 258, + 259, + 262, + 271, + 272, + 273, + 277, + 278, + 279, + 282, + 283, + 284, + 296, + 297, + 305, + 339, + 700, + 34665, + 34853, + 50735, + ] + assert exif[256] == 640 + assert exif[271] == "FLIR" + + gps = exif.get_ifd(0x8825) + assert list(gps.keys()) == [0, 1, 2, 3, 4, 5, 6, 18] + assert gps[0] == b"\x03\x02\x00\x00" + assert gps[18] == "WGS-84" + + def test_exif_frames(self): + # Test that EXIF data can change across frames + with Image.open("Tests/images/g4-multi.tiff") as im: + assert im.getexif()[273] == (328, 815) + + im.seek(1) + assert im.getexif()[273] == (1408, 1907) + def test_seek(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: diff --git a/Tests/test_image.py b/Tests/test_image.py index 82efefc1e..b6e5873f4 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -773,6 +773,27 @@ class TestImage: reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) + def test_exif_load_from_fp(self): + with Image.open("Tests/images/flower.jpg") as im: + data = im.info["exif"] + if data.startswith(b"Exif\x00\x00"): + data = data[6:] + fp = io.BytesIO(data) + + exif = Image.Exif() + exif.load_from_fp(fp) + assert exif == { + 271: "Canon", + 272: "Canon PowerShot S40", + 274: 1, + 282: 180.0, + 283: 180.0, + 296: 2, + 306: "2003:12:14 12:01:44", + 531: 1, + 34665: 196, + } + @pytest.mark.skipif( sys.version_info < (3, 7), reason="Python 3.7 or greater required" ) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6529db2be..c9153a4d2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1317,11 +1317,16 @@ class Image: self._exif = Exif() exif_info = self.info.get("exif") - if exif_info is None and "Raw profile type exif" in self.info: - exif_info = bytes.fromhex( - "".join(self.info["Raw profile type exif"].split("\n")[3:]) - ) - self._exif.load(exif_info) + if exif_info is None: + if "Raw profile type exif" in self.info: + exif_info = bytes.fromhex( + "".join(self.info["Raw profile type exif"].split("\n")[3:]) + ) + elif hasattr(self, "tag_v2"): + self._exif.endian = self.tag_v2._endian + self._exif.load_from_fp(self.fp, self.tag_v2._offset) + if exif_info is not None: + self._exif.load(exif_info) # XMP tags if 0x0112 not in self._exif: @@ -3297,7 +3302,7 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): - endian = "<" + endian = None def __init__(self): self._data = {} @@ -3332,6 +3337,12 @@ class Exif(MutableMapping): info.load(self.fp) return self._fixup_dict(info) + def _get_head(self): + if self.endian == "<": + return b"II\x2A\x00\x08\x00\x00\x00" + else: + return b"MM\x00\x2A\x00\x00\x00\x08" + def load(self, data): # Extract EXIF information. This is highly experimental, # and is likely to be replaced with something better in a future @@ -3344,8 +3355,8 @@ class Exif(MutableMapping): self._loaded_exif = data self._data.clear() self._ifds.clear() - self._info = None if not data: + self._info = None return if data.startswith(b"Exif\x00\x00"): @@ -3360,6 +3371,27 @@ class Exif(MutableMapping): self.fp.seek(self._info.next) self._info.load(self.fp) + def load_from_fp(self, fp, offset=None): + self._loaded_exif = None + self._data.clear() + self._ifds.clear() + + # process dictionary + from . import TiffImagePlugin + + self.fp = fp + if offset is not None: + self.head = self._get_head() + else: + self.head = self.fp.read(8) + self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) + if self.endian is None: + self.endian = self._info._endian + if offset is None: + offset = self._info.next + self.fp.seek(offset) + self._info.load(self.fp) + def _get_merged_dict(self): merged_dict = dict(self) @@ -3378,10 +3410,7 @@ class Exif(MutableMapping): def tobytes(self, offset=8): from . import TiffImagePlugin - if self.endian == "<": - head = b"II\x2A\x00\x08\x00\x00\x00" - else: - head = b"MM\x00\x2A\x00\x00\x00\x08" + head = self._get_head() ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) for tag, value in self.items(): if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict):