From 0b405c86beee92dec2acd2f7ad1d1f72a5ac9533 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 18 Aug 2019 23:03:43 +1000 Subject: [PATCH 1/4] Lazily use ImageFileDirectory_v1 values from Exif --- src/PIL/Image.py | 83 ++++++++++++++++++++++++-------------- src/PIL/JpegImagePlugin.py | 2 +- src/PIL/MpoImagePlugin.py | 2 +- 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 8b92aae45a7..d652bd978e5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3137,25 +3137,26 @@ class Exif(MutableMapping): def __init__(self): self._data = {} self._ifds = {} + self.info = None + + def _fixup(self, value): + try: + if len(value) == 1 and not isinstance(value, dict): + return value[0] + except Exception: + pass + return value def _fixup_dict(self, src_dict): # Helper function for _getexif() # returns a dict with any single item tuples/lists as individual values - def _fixup(value): - try: - if len(value) == 1 and not isinstance(value, dict): - return value[0] - except Exception: - pass - return value - - return {k: _fixup(v) for k, v in src_dict.items()} + return {k: self._fixup(v) for k, v in src_dict.items()} def _get_ifd_dict(self, tag): try: # an offset pointer to the location of the nested embedded IFD. # It should be a long, but may be corrupted. - self.fp.seek(self._data[tag]) + self.fp.seek(self[tag]) except (KeyError, TypeError): pass else: @@ -3177,11 +3178,10 @@ def load(self, data): # process dictionary from . import TiffImagePlugin - info = TiffImagePlugin.ImageFileDirectory_v1(self.head) - self.endian = info._endian - self.fp.seek(info.next) - info.load(self.fp) - self._data = dict(self._fixup_dict(info)) + self.info = TiffImagePlugin.ImageFileDirectory_v1(self.head) + self.endian = self.info._endian + self.fp.seek(self.info.next) + self.info.load(self.fp) # get EXIF extension ifd = self._get_ifd_dict(0x8769) @@ -3189,12 +3189,6 @@ def load(self, data): self._data.update(ifd) self._ifds[0x8769] = ifd - # get gpsinfo extension - ifd = self._get_ifd_dict(0x8825) - if ifd: - self._data[0x8825] = ifd - self._ifds[0x8825] = ifd - def tobytes(self, offset=0): from . import TiffImagePlugin @@ -3203,19 +3197,20 @@ def tobytes(self, offset=0): else: head = b"MM\x00\x2A\x00\x00\x00\x08" ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) - for tag, value in self._data.items(): + for tag, value in self.items(): ifd[tag] = value return b"Exif\x00\x00" + head + ifd.tobytes(offset) def get_ifd(self, tag): - if tag not in self._ifds and tag in self._data: - if tag == 0xA005: # interop + if tag not in self._ifds and tag in self: + if tag in [0x8825, 0xA005]: + # gpsinfo, interop self._ifds[tag] = self._get_ifd_dict(tag) elif tag == 0x927C: # makernote from .TiffImagePlugin import ImageFileDirectory_v2 - if self._data[0x927C][:8] == b"FUJIFILM": - exif_data = self._data[0x927C] + if self[0x927C][:8] == b"FUJIFILM": + exif_data = self[0x927C] ifd_offset = i32le(exif_data[8:12]) ifd_data = exif_data[ifd_offset:] @@ -3252,8 +3247,8 @@ def get_ifd(self, tag): ImageFileDirectory_v2(), data, False ) self._ifds[0x927C] = dict(self._fixup_dict(makernote)) - elif self._data.get(0x010F) == "Nintendo": - ifd_data = self._data[0x927C] + elif self.get(0x010F) == "Nintendo": + ifd_data = self[0x927C] makernote = {} for i in range(0, struct.unpack(">H", ifd_data[:2])[0]): @@ -3291,16 +3286,29 @@ def get_ifd(self, tag): return self._ifds.get(tag, {}) def __str__(self): + if self.info is not None: + # Load all keys into self._data + for tag in self.info.keys(): + self[tag] + return str(self._data) def __len__(self): - return len(self._data) + keys = set(self._data) + if self.info is not None: + keys.update(self.info) + return len(keys) def __getitem__(self, tag): + if self.info is not None and tag not in self._data and tag in self.info: + self._data[tag] = self._fixup(self.info[tag]) + if tag == 0x8825: + self._data[tag] = self.get_ifd(tag) + del self.info[tag] return self._data[tag] def __contains__(self, tag): - return tag in self._data + return tag in self._data or (self.info is not None and tag in self.info) if not py3: @@ -3308,10 +3316,23 @@ def has_key(self, tag): return tag in self def __setitem__(self, tag, value): + if self.info is not None: + try: + del self.info[tag] + except KeyError: + pass self._data[tag] = value def __delitem__(self, tag): + if self.info is not None: + try: + del self.info[tag] + except KeyError: + pass del self._data[tag] def __iter__(self): - return iter(set(self._data)) + keys = set(self._data) + if self.info is not None: + keys.update(self.info) + return iter(keys) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index f1a2f78132a..1770fc2105b 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -158,7 +158,7 @@ def APP(self, marker): # If DPI isn't in JPEG header, fetch from EXIF if "dpi" not in self.info and "exif" in self.info: try: - exif = self._getexif() + exif = self.getexif() resolution_unit = exif[0x0128] x_resolution = exif[0x011A] try: diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 81b37172a18..24e953013dc 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -92,7 +92,7 @@ def seek(self, frame): n = i16(self.fp.read(2)) - 2 self.info["exif"] = ImageFile._safe_read(self.fp, n) - exif = self._getexif() + exif = self.getexif() if 40962 in exif and 40963 in exif: self._size = (exif[40962], exif[40963]) elif "exif" in self.info: From f3ed44a5662ea2728fae2139bbdde9419356302a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Aug 2019 06:13:20 +1000 Subject: [PATCH 2/4] Changed the Image getexif method to return a shared Exif instance --- src/PIL/Image.py | 57 +++++++++++++++++++++++--------------- src/PIL/JpegImagePlugin.py | 12 +------- src/PIL/MpoImagePlugin.py | 2 -- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d652bd978e5..7c6a46fa74b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -555,6 +555,7 @@ def __init__(self): self.category = NORMAL self.readonly = 0 self.pyaccess = None + self._exif = None @property def width(self): @@ -1324,10 +1325,10 @@ def getextrema(self): return self.im.getextrema() def getexif(self): - exif = Exif() - if "exif" in self.info: - exif.load(self.info["exif"]) - return exif + if self._exif is None: + self._exif = Exif() + self._exif.load(self.info.get("exif")) + return self._exif def getim(self): """ @@ -3137,7 +3138,8 @@ class Exif(MutableMapping): def __init__(self): self._data = {} self._ifds = {} - self.info = None + self._info = None + self._loaded_exif = None def _fixup(self, value): try: @@ -3173,15 +3175,24 @@ def load(self, data): # The EXIF record consists of a TIFF file embedded in a JPEG # application marker (!). + if data == self._loaded_exif: + return + self._loaded_exif = data + self._data.clear() + self._ifds.clear() + self._info = None + if not data: + return + self.fp = io.BytesIO(data[6:]) self.head = self.fp.read(8) # process dictionary from . import TiffImagePlugin - self.info = TiffImagePlugin.ImageFileDirectory_v1(self.head) - self.endian = self.info._endian - self.fp.seek(self.info.next) - self.info.load(self.fp) + self._info = TiffImagePlugin.ImageFileDirectory_v1(self.head) + self.endian = self._info._endian + self.fp.seek(self._info.next) + self._info.load(self.fp) # get EXIF extension ifd = self._get_ifd_dict(0x8769) @@ -3286,29 +3297,29 @@ def get_ifd(self, tag): return self._ifds.get(tag, {}) def __str__(self): - if self.info is not None: + if self._info is not None: # Load all keys into self._data - for tag in self.info.keys(): + for tag in self._info.keys(): self[tag] return str(self._data) def __len__(self): keys = set(self._data) - if self.info is not None: - keys.update(self.info) + if self._info is not None: + keys.update(self._info) return len(keys) def __getitem__(self, tag): - if self.info is not None and tag not in self._data and tag in self.info: - self._data[tag] = self._fixup(self.info[tag]) + if self._info is not None and tag not in self._data and tag in self._info: + self._data[tag] = self._fixup(self._info[tag]) if tag == 0x8825: self._data[tag] = self.get_ifd(tag) - del self.info[tag] + del self._info[tag] return self._data[tag] def __contains__(self, tag): - return tag in self._data or (self.info is not None and tag in self.info) + return tag in self._data or (self._info is not None and tag in self._info) if not py3: @@ -3316,23 +3327,23 @@ def has_key(self, tag): return tag in self def __setitem__(self, tag, value): - if self.info is not None: + if self._info is not None: try: - del self.info[tag] + del self._info[tag] except KeyError: pass self._data[tag] = value def __delitem__(self, tag): - if self.info is not None: + if self._info is not None: try: - del self.info[tag] + del self._info[tag] except KeyError: pass del self._data[tag] def __iter__(self): keys = set(self._data) - if self.info is not None: - keys.update(self.info) + if self._info is not None: + keys.update(self._info) return iter(keys) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 1770fc2105b..020b952192f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -485,19 +485,9 @@ def _fixup_dict(src_dict): def _getexif(self): - # Use the cached version if possible - try: - return self.info["parsed_exif"] - except KeyError: - pass - if "exif" not in self.info: return None - exif = dict(self.getexif()) - - # Cache the result for future use - self.info["parsed_exif"] = exif - return exif + return dict(self.getexif()) def _getmp(self): diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 24e953013dc..938f2a5a646 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -86,8 +86,6 @@ def seek(self, frame): self.offset = self.__mpoffsets[frame] self.fp.seek(self.offset + 2) # skip SOI marker - if "parsed_exif" in self.info: - del self.info["parsed_exif"] if i16(self.fp.read(2)) == 0xFFE1: # APP1 n = i16(self.fp.read(2)) - 2 self.info["exif"] = ImageFile._safe_read(self.fp, n) From ef16cb8efe6a9c12034d2343783b51beb0ff9ba5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2019 18:31:23 +1000 Subject: [PATCH 3/4] ImageFileDirectory_v1 does not raise KeyError --- src/PIL/Image.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7c6a46fa74b..b0ba266e0a2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3327,19 +3327,13 @@ def has_key(self, tag): return tag in self def __setitem__(self, tag, value): - if self._info is not None: - try: - del self._info[tag] - except KeyError: - pass + if self._info is not None and tag in self._info: + del self._info[tag] self._data[tag] = value def __delitem__(self, tag): - if self._info is not None: - try: - del self._info[tag] - except KeyError: - pass + if self._info is not None and tag in self._info: + del self._info[tag] del self._data[tag] def __iter__(self): From 5a668779e95142238fd6cd9976b6b2e9153ec9a2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 8 Sep 2019 21:27:55 +1000 Subject: [PATCH 4/4] Added tests --- Tests/test_imagefile.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 2ca5abe4ca5..f24f9deab57 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -321,3 +321,13 @@ def test_exif_interop(self): self.assertEqual( exif.get_ifd(0xA005), {1: "R98", 2: b"0100", 4097: 2272, 4098: 1704} ) + + def test_exif_shared(self): + im = Image.open("Tests/images/exif.png") + exif = im.getexif() + self.assertIs(im.getexif(), exif) + + def test_exif_str(self): + im = Image.open("Tests/images/exif.png") + exif = im.getexif() + self.assertEqual(str(exif), "{274: 1}")