Repository: python-pillow/Pillow

Issue:
Metadata Warning, tag 34853 had too many entries
### What did you do?
Open an image and then use ImageOps.exif_transpose operation.

### What did you expect to happen?
No warning.

### What actually happened?
```
/home/zeyang/anaconda3/lib/python3.7/site-packages/PIL/TiffImagePlugin.py:594: UserWarning: Metadata Warning, tag 34853 had too many entries: 10, expected 1
  % (tag, len(values))
```
### What are your OS, Python and Pillow versions?

* OS: ubuntu 16.04
* Python: 3.7.1
* Pillow: 7.2.0

```python
# codes to reproduce the warning
from PIL import Image, ImageOps
img = Image.open('d9af31bc74d18334.jpg')
img = ImageOps.exif_transpose(img)
```

[Image](https://user-images.githubusercontent.com/42920710/91961072-471b9780-ed3d-11ea-969b-cffc4af119b4.jpg) used for reproducing this error.


Discussion:

Resolving patch:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -567,7 +567,9 @@ def _setitem(self, tag, value, legacy_api):
         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
 
@@ -576,7 +578,7 @@ def _setitem(self, tag, value, legacy_api):
         # 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)
@@ -802,11 +804,22 @@ def tobytes(self, offset=0):
                 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: " + (
                 "<table: %d bytes>" % len(data) if len(data) >= 16 else str(values)
@@ -814,7 +827,9 @@ def tobytes(self, offset=0):
             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)


Test patch:
diff --git a/Tests/images/empty_gps_ifd.jpg b/Tests/images/empty_gps_ifd.jpg
new file mode 100644
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
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -8,6 +8,7 @@
     ExifTags,
     Image,
     ImageFile,
+    ImageOps,
     JpegImagePlugin,
     UnidentifiedImageError,
     features,
@@ -224,23 +225,48 @@ def test_exif_typeerror(self):
             # Should not raise a TypeError
             im._getexif()
 
-    def test_exif_gps(self):
-        # Arrange
-        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",
-            }
+    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
 
-            # Act
+        # Reading
+        with Image.open("Tests/images/exif_gps.jpg") as im:
             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_rollback(self):
         # rolling back exif support in 3.1 to pre-3.0 formatting.