Repository: python-pillow/Pillow

Issue:
Bug: TypeError exif_transpose() / exif.tobytes() 
Doing `PIL.ImageOps.exif_transpose(im)` for a .jpg I get this error:

```pytb
  File "C:\Python38\lib\site-packages\PIL\ImageOps.py", line 602, in exif_transpose
    transposed_image.info["exif"] = transposed_exif.tobytes()
  File "C:\Python38\lib\site-packages\PIL\Image.py", line 3628, in tobytes
    return b"Exif\x00\x00" + head + ifd.tobytes(offset)
  File "C:\Python38\lib\site-packages\PIL\TiffImagePlugin.py", line 878, in tobytes
    data = ifd.tobytes(offset)
  File "C:\Python38\lib\site-packages\PIL\TiffImagePlugin.py", line 887, in tobytes
    "<table: %d bytes>" % len(data) if len(data) >= 16 else str(values)
TypeError: object of type 'IFDRational' has no len()

```
At post-mortem:
```pycon
>>> PIL.__version__
'9.3.0'
>>> data
2.2
>>> type(data)
<class 'PIL.TiffImagePlugin.IFDRational'>
>>> len(data)
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
TypeError: object of type 'IFDRational' has no len()

# stack frame `data = ifd.tobytes(offset)`
>>> tag, value
(34853, {5: 2.2, 6: 0.0})   # GPSInfo
>>> type(value[5])
<class 'PIL.TiffImagePlugin.IFDRational'>

# stack frame with image:
>>> im.getexif()[274]  # Orientation
6
```

(Can't publish the image / exif unfortunately, but it should be clear whats going on: IFDRational in GPS exif tag fails to serialize to bytes)



Discussion:
So you've found an image where the [GPSAltitudeRef](https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/gps/gpsaltituderef.html), which should either be a BYTE of 0 (meaning "above sea level") or 1 (meaning "below sea level"), is instead 2.2.

Thanks for trying to be helpful in the absence of an image. Do you know what software created this image? Out of curiosity, would you also be able to let us know what is the value of `typ`?
> Do you know what software created this image? Out of curiosity, would you also be able to let us know what is the value of `typ`?

It seems to be directly from a phone camera (LG).
`typ` and other locals():

```pytb
>>> im._exif.tobytes()
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
  File "C:\Python38\lib\site-packages\PIL\Image.py", line 3628, in tobytes
    return b"Exif\x00\x00" + head + ifd.tobytes(offset)
  File "C:\Python38\lib\site-packages\PIL\TiffImagePlugin.py", line 878, in tobytes
    data = ifd.tobytes(offset)
  File "C:\Python38\lib\site-packages\PIL\TiffImagePlugin.py", line 887, in tobytes
    "<table: %d bytes>" % len(data) if len(data) >= 16 else str(values)
TypeError: object of type 'IFDRational' has no len()
>>> pm()
DBPE>>> typ
1
DBPE>>> locals()
{'self': <PIL.TiffImagePlugin.ImageFileDirectory_v2 object at 0x0000022405E20D30>, 'offset': 1328, 'result': b'\x00\x02', 'entries': [], 'stripoffsets': None, 'tag': 5, 'value': 2.2, 'typ': 1, 'is_ifd': False, 'values': (2.2,), 'data': 2.2, 'tagname': 'GPSAltitudeRef', 'typname': 'byte', 'msg': 'save: GPSAltitudeRef (5) - type: byte (1)'}
DBPE>>> self.tagtype
{5: 1, 6: 5}
DBPE>>> self._write_dispatch[typ](self, *values)
2.2
DBPE>>> 
```

The typ 1 seems to be forced by these lines in `_setitem()`  while self.group is 34853:
```python
        info = TiffTags.lookup(tag, self.group)
        ...
            if info.type:
                self.tagtype[tag] = info.type

```
( When the data is extracted the same lines were passed for that tag,  but self.group then is None, thus auto detection as IFDRational and  `self.tagtype == {5: 5, 6: 5}` )


Resolving patch:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -722,6 +722,8 @@ def load_byte(self, data, legacy_api=True):
 
     @_register_writer(1)  # Basic type, except for the legacy API.
     def write_byte(self, data):
+        if isinstance(data, IFDRational):
+            data = int(data)
         if isinstance(data, int):
             data = bytes((data,))
         return data


Test patch:
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -202,14 +202,15 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path):
         assert reloaded.tag_v2[271] == expected
 
 
-def test_writing_int_to_bytes(tmp_path):
+@pytest.mark.parametrize("value", (1, IFDRational(1)))
+def test_writing_other_types_to_bytes(value, tmp_path):
     im = hopper()
     info = TiffImagePlugin.ImageFileDirectory_v2()
 
     tag = TiffTags.TAGS_V2[700]
     assert tag.type == TiffTags.BYTE
 
-    info[700] = 1
+    info[700] = value
 
     out = str(tmp_path / "temp.tiff")
     im.save(out, tiffinfo=info)