diff --git a/Tests/images/empty_gps_ifd.jpg b/Tests/images/empty_gps_ifd.jpg new file mode 100644 index 000000000..28f180b87 Binary files /dev/null and b/Tests/images/empty_gps_ifd.jpg differ diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 717794614..78d588dec 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -8,6 +8,7 @@ from PIL import ( ExifTags, Image, ImageFile, + ImageOps, JpegImagePlugin, UnidentifiedImageError, features, @@ -225,23 +226,48 @@ class TestFileJpeg: # Should not raise a TypeError im._getexif() - def test_exif_gps(self): - # Arrange + def test_exif_gps(self, tmp_path): + expected_exif_gps = { + 0: b"\x00\x00\x00\x01", + 2: 4294967295, + 5: b"\x01", + 30: 65535, + 29: "1999:99:99 99:99:99", + } + gps_index = 34853 + + # Reading with Image.open("Tests/images/exif_gps.jpg") as im: - gps_index = 34853 - expected_exif_gps = { - 0: b"\x00\x00\x00\x01", - 2: 4294967295, - 5: b"\x01", - 30: 65535, - 29: "1999:99:99 99:99:99", - } - - # Act exif = im._getexif() + assert exif[gps_index] == expected_exif_gps - # Assert - assert exif[gps_index] == expected_exif_gps + # Writing + f = str(tmp_path / "temp.jpg") + exif = Image.Exif() + exif[gps_index] = expected_exif_gps + hopper().save(f, exif=exif) + + with Image.open(f) as reloaded: + exif = reloaded._getexif() + assert exif[gps_index] == expected_exif_gps + + def test_empty_exif_gps(self): + with Image.open("Tests/images/empty_gps_ifd.jpg") as im: + exif = im.getexif() + del exif[0x8769] + + # Assert that it needs to be transposed + assert exif[0x0112] == Image.TRANSVERSE + + # Assert that the GPS IFD is present and empty + assert exif[0x8825] == {} + + transposed = ImageOps.exif_transpose(im) + exif = transposed.getexif() + assert exif[0x8825] == {} + + # Assert that it was transposed + assert 0x0112 not in exif def test_exif_equality(self): # In 7.2.0, Exif rationals were changed to be read as diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 57f08d98c..0d91aa6a6 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -569,7 +569,9 @@ class ImageFileDirectory_v2(MutableMapping): elif self.tagtype[tag] == TiffTags.RATIONAL: values = [float(v) if isinstance(v, int) else v for v in values] - values = tuple(info.cvt_enum(value) for value in values) + is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) + if not is_ifd: + values = tuple(info.cvt_enum(value) for value in values) dest = self._tags_v1 if legacy_api else self._tags_v2 @@ -578,7 +580,7 @@ class ImageFileDirectory_v2(MutableMapping): # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. # Don't mess with the legacy api, since it's frozen. - if ( + if not is_ifd and ( (info.length == 1) or self.tagtype[tag] == TiffTags.BYTE or (info.length is None and len(values) == 1 and not legacy_api) @@ -804,11 +806,22 @@ class ImageFileDirectory_v2(MutableMapping): stripoffsets = len(entries) typ = self.tagtype.get(tag) logger.debug("Tag {}, Type: {}, Value: {}".format(tag, typ, value)) - values = value if isinstance(value, tuple) else (value,) - data = self._write_dispatch[typ](self, *values) + is_ifd = typ == TiffTags.LONG and isinstance(value, dict) + if is_ifd: + if self._endian == "<": + ifh = b"II\x2A\x00\x08\x00\x00\x00" + else: + ifh = b"MM\x00\x2A\x00\x00\x00\x08" + ifd = ImageFileDirectory_v2(ifh) + for ifd_tag, ifd_value in self._tags_v2[tag].items(): + ifd[ifd_tag] = ifd_value + data = ifd.tobytes(offset) + else: + values = value if isinstance(value, tuple) else (value,) + data = self._write_dispatch[typ](self, *values) tagname = TiffTags.lookup(tag).name - typname = TYPES.get(typ, "unknown") + typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") msg = "save: %s (%d) - type: %s (%d)" % (tagname, tag, typname, typ) msg += " - value: " + ( "" % len(data) if len(data) >= 16 else str(values) @@ -816,7 +829,9 @@ class ImageFileDirectory_v2(MutableMapping): logger.debug(msg) # count is sum of lengths for string and arbitrary data - if typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: + if is_ifd: + count = 1 + elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: count = len(data) else: count = len(values)