From faf8fad76d5f4a2ec9a41bc92fad106b35a613f1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Oct 2020 20:35:45 +1100 Subject: [PATCH 1/6] Stopped flattening EXIF IFD into getexif() --- Tests/test_image.py | 24 ++--- src/PIL/Image.py | 182 ++++++++++++++++++++----------------- src/PIL/JpegImagePlugin.py | 2 +- src/PIL/MpoImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 2 +- src/PIL/WebPImagePlugin.py | 2 +- 6 files changed, 113 insertions(+), 101 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 73cf7bf8350..3d080495031 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -663,43 +663,43 @@ def test_exif_jpeg(self, tmp_path): exif = im.getexif() assert 258 not in exif assert 274 in exif - assert 40960 in exif - assert exif[40963] == 450 + assert 282 in exif + assert exif[296] == 2 assert exif[11] == "gThumb 3.0.1" out = str(tmp_path / "temp.jpg") exif[258] = 8 del exif[274] - del exif[40960] - exif[40963] = 455 + del exif[282] + exif[296] = 455 exif[11] = "Pillow test" im.save(out, exif=exif) with Image.open(out) as reloaded: reloaded_exif = reloaded.getexif() assert reloaded_exif[258] == 8 assert 274 not in reloaded_exif - assert 40960 not in reloaded_exif - assert reloaded_exif[40963] == 455 + assert 282 not in reloaded_exif + assert reloaded_exif[296] == 455 assert reloaded_exif[11] == "Pillow test" with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: # Big endian exif = im.getexif() assert 258 not in exif - assert 40962 in exif - assert exif[40963] == 200 + assert 306 in exif + assert exif[274] == 1 assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)" out = str(tmp_path / "temp.jpg") exif[258] = 8 - del exif[34665] - exif[40963] = 455 + del exif[306] + exif[274] = 455 exif[305] = "Pillow test" im.save(out, exif=exif) with Image.open(out) as reloaded: reloaded_exif = reloaded.getexif() assert reloaded_exif[258] == 8 - assert 34665 not in reloaded_exif - assert reloaded_exif[40963] == 455 + assert 306 not in reloaded_exif + assert reloaded_exif[274] == 455 assert reloaded_exif[305] == "Pillow test" @skip_unless_feature("webp") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 01fe7ed1bae..354dbbed7f1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3309,11 +3309,11 @@ def _fixup_dict(self, src_dict): # returns a dict with any single item tuples/lists as individual values return {k: self._fixup(v) for k, v in src_dict.items()} - def _get_ifd_dict(self, tag): + def _get_ifd_dict(self, offset): 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[tag]) + self.fp.seek(offset) except (KeyError, TypeError): pass else: @@ -3351,11 +3351,16 @@ def load(self, data): self.fp.seek(self._info.next) self._info.load(self.fp) + def _get_merged_dict(self): + merged_dict = dict(self) + # get EXIF extension - ifd = self._get_ifd_dict(0x8769) - if ifd: - self._data.update(ifd) - self._ifds[0x8769] = ifd + if 0x8769 in self: + ifd = self._get_ifd_dict(self[0x8769]) + if ifd: + merged_dict.update(ifd) + + return merged_dict def tobytes(self, offset=8): from . import TiffImagePlugin @@ -3370,87 +3375,94 @@ def tobytes(self, offset=8): return b"Exif\x00\x00" + head + ifd.tobytes(offset) def get_ifd(self, tag): - 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[0x927C][:8] == b"FUJIFILM": - exif_data = self[0x927C] - ifd_offset = i32le(exif_data, 8) - ifd_data = exif_data[ifd_offset:] - - makernote = {} - for i in range(0, struct.unpack(" 4: - (offset,) = struct.unpack("H", ifd_data[:2])[0]): - ifd_tag, typ, count, data = struct.unpack( - ">HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2] - ) - if ifd_tag == 0x1101: - # CameraInfo - (offset,) = struct.unpack(">L", data) - self.fp.seek(offset) - - camerainfo = {"ModelID": self.fp.read(4)} - - self.fp.read(4) - # Seconds since 2000 - camerainfo["TimeStamp"] = i32le(self.fp.read(12)) - - self.fp.read(4) - camerainfo["InternalSerialNumber"] = self.fp.read(4) - - self.fp.read(12) - parallax = self.fp.read(4) - handler = ImageFileDirectory_v2._load_dispatch[ - TiffTags.FLOAT - ][1] - camerainfo["Parallax"] = handler( - ImageFileDirectory_v2(), parallax, False + try: + ( + unit_size, + handler, + ) = ImageFileDirectory_v2._load_dispatch[typ] + except KeyError: + continue + size = count * unit_size + if size > 4: + (offset,) = struct.unpack("H", tag_data[:2])[0]): + ifd_tag, typ, count, data = struct.unpack( + ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] + ) + if ifd_tag == 0x1101: + # CameraInfo + (offset,) = struct.unpack(">L", data) + self.fp.seek(offset) + + camerainfo = {"ModelID": self.fp.read(4)} + + self.fp.read(4) + # Seconds since 2000 + camerainfo["TimeStamp"] = i32le(self.fp.read(12)) + + self.fp.read(4) + camerainfo["InternalSerialNumber"] = self.fp.read(4) + + self.fp.read(12) + parallax = self.fp.read(4) + handler = ImageFileDirectory_v2._load_dispatch[ + TiffTags.FLOAT + ][1] + camerainfo["Parallax"] = handler( + ImageFileDirectory_v2(), parallax, False + ) + + self.fp.read(4) + camerainfo["Category"] = self.fp.read(2) + + makernote = {0x1101: dict(self._fixup_dict(camerainfo))} + self._ifds[tag] = makernote + else: + # gpsinfo, interop + self._ifds[tag] = self._get_ifd_dict(tag_data) return self._ifds.get(tag, {}) def __str__(self): diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 054495e6f6a..ad260acbd5a 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -478,7 +478,7 @@ def _getmp(self): def _getexif(self): if "exif" not in self.info: return None - return dict(self.getexif()) + return self.getexif()._get_merged_dict() def _getmp(self): diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 575cc9c8ec6..8b49d10e513 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -82,7 +82,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().get_ifd(0x8769) if 40962 in exif and 40963 in exif: self._size = (exif[40962], exif[40963]) elif "exif" in self.info: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 2d4ac760661..30eb13aa395 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -968,7 +968,7 @@ def _getexif(self): self.load() if "exif" not in self.info and "Raw profile type exif" not in self.info: return None - return dict(self.getexif()) + return self.getexif()._get_merged_dict() def getexif(self): if "exif" not in self.info: diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 2e9746fa3ff..bc12ce4beb8 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -96,7 +96,7 @@ def _open(self): def _getexif(self): if "exif" not in self.info: return None - return dict(self.getexif()) + return self.getexif()._get_merged_dict() def seek(self, frame): if not self._seek_check(frame): From 4b14f0102d8fa585c900ff8a174defb2452c042b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Oct 2020 20:16:48 +1100 Subject: [PATCH 2/6] Save base IFDs when converting Exif to bytes --- Tests/test_image.py | 8 ++++++++ src/PIL/Image.py | 2 ++ 2 files changed, 10 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index 3d080495031..b1db41235dc 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -752,6 +752,14 @@ def test_exif_interop(self): 4098: 1704, } + def test_exif_ifd(self): + im = Image.open("Tests/images/flower.jpg") + exif = im.getexif() + + reloaded_exif = Image.Exif() + reloaded_exif.load(exif.tobytes()) + assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) + @pytest.mark.parametrize( "test_module", [PIL, Image], diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 354dbbed7f1..73eef3d81f5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3371,6 +3371,8 @@ def tobytes(self, offset=8): head = b"MM\x00\x2A\x00\x00\x00\x08" ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) for tag, value in self.items(): + if tag in [0x8769, 0x8225] and not isinstance(value, dict): + value = self.get_ifd(tag) ifd[tag] = value return b"Exif\x00\x00" + head + ifd.tobytes(offset) From b25bc400093283104e7eeb232074795f2e2cdb8e Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 7 Oct 2020 18:35:16 +1100 Subject: [PATCH 3/6] Simplified code Co-authored-by: Konstantin Kopachev --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 73eef3d81f5..d318bc236b0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3390,7 +3390,7 @@ def get_ifd(self, tag): if tag == 0x927C: from .TiffImagePlugin import ImageFileDirectory_v2 - if self._ifds[0x8769][tag][:8] == b"FUJIFILM": + if tag_data[:8] == b"FUJIFILM": ifd_offset = i32le(tag_data, 8) ifd_data = tag_data[ifd_offset:] From e763f8f2be026cd598f0e1bd3c8b50c5b2d9be64 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Feb 2021 07:47:59 +1100 Subject: [PATCH 4/6] Save interop IFD when converting Exif to bytes --- Tests/test_image.py | 9 +++++++-- src/PIL/Image.py | 7 +++++++ src/PIL/TiffTags.py | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index b1db41235dc..e1c14d0d896 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -752,9 +752,14 @@ def test_exif_interop(self): 4098: 1704, } + reloaded_exif = Image.Exif() + reloaded_exif.load(exif.tobytes()) + assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005) + def test_exif_ifd(self): - im = Image.open("Tests/images/flower.jpg") - exif = im.getexif() + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + del exif.get_ifd(0x8769)[0xA005] reloaded_exif = Image.Exif() reloaded_exif.load(exif.tobytes()) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d318bc236b0..df3ebfd18a6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3373,6 +3373,13 @@ def tobytes(self, offset=8): for tag, value in self.items(): if tag in [0x8769, 0x8225] and not isinstance(value, dict): value = self.get_ifd(tag) + if ( + tag == 0x8769 + and 0xA005 in value + and not isinstance(value[0xA005], dict) + ): + value = value.copy() + value[0xA005] = self.get_ifd(0xA005) ifd[tag] = value return b"Exif\x00\x00" + head + ifd.tobytes(offset) diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 796ff34795d..9e9e117a47b 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -184,6 +184,7 @@ def lookup(tag): 34665: ("ExifIFD", LONG, 1), 34675: ("ICCProfile", UNDEFINED, 1), 34853: ("GPSInfoIFD", LONG, 1), + 40965: ("InteroperabilityIFD", LONG, 1), # MPInfo 45056: ("MPFVersion", UNDEFINED, 1), 45057: ("NumberOfImages", LONG, 1), From c52b45df62a34b14c66159db777e9d3fc942fb55 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Mar 2021 12:32:42 +1100 Subject: [PATCH 5/6] Removed automatic retrieval of GPS IFD --- Tests/test_file_jpeg.py | 4 ++-- src/PIL/Image.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 740f9fa4d73..3ee33d65f11 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -264,11 +264,11 @@ def test_empty_exif_gps(self): assert exif[0x0112] == Image.TRANSVERSE # Assert that the GPS IFD is present and empty - assert exif[0x8825] == {} + assert exif.get_ifd(0x8825) == {} transposed = ImageOps.exif_transpose(im) exif = transposed.getexif() - assert exif[0x8825] == {} + assert exif.get_ifd(0x8825) == {} # Assert that it was transposed assert 0x0112 not in exif diff --git a/src/PIL/Image.py b/src/PIL/Image.py index df3ebfd18a6..31eab54a4b9 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3360,6 +3360,10 @@ def _get_merged_dict(self): if ifd: merged_dict.update(ifd) + # GPS + if 0x8825 in self: + merged_dict[0x8825] = self._get_ifd_dict(self[0x8825]) + return merged_dict def tobytes(self, offset=8): @@ -3371,7 +3375,7 @@ def tobytes(self, offset=8): head = b"MM\x00\x2A\x00\x00\x00\x08" ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) for tag, value in self.items(): - if tag in [0x8769, 0x8225] and not isinstance(value, dict): + if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict): value = self.get_ifd(tag) if ( tag == 0x8769 @@ -3491,8 +3495,6 @@ def __len__(self): 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] From 36a4b055bba2c2ffff6945f0bb071ac1554c26ac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Mar 2021 12:50:30 +1100 Subject: [PATCH 6/6] Updated comments --- src/PIL/Image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 545fdc019bc..2e7abfb68c9 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3397,6 +3397,7 @@ def get_ifd(self, tag): self.get_ifd(0x8769) tag_data = self._ifds[0x8769][tag] if tag == 0x927C: + # makernote from .TiffImagePlugin import ImageFileDirectory_v2 if tag_data[:8] == b"FUJIFILM": @@ -3472,7 +3473,7 @@ def get_ifd(self, tag): makernote = {0x1101: dict(self._fixup_dict(camerainfo))} self._ifds[tag] = makernote else: - # gpsinfo, interop + # interop self._ifds[tag] = self._get_ifd_dict(tag_data) return self._ifds.get(tag, {})