Repository: python-pillow/Pillow

Issue:
TIFF Image tags are not available
<!--
Thank you for reporting an issue.

Follow these guidelines to ensure your issue is handled properly.

If you have a ...

1.  General question: consider asking the question on Stack Overflow
    with the python-imaging-library tag:

    * https://stackoverflow.com/questions/tagged/python-imaging-library

    Do not ask a question in both places.

    If you think you have found a bug or have an unexplained exception
    then file a bug report here.

2.  Bug report: include a self-contained, copy-pastable example that
    generates the issue if possible. Be concise with code posted.
    Guidelines on how to provide a good bug report:

    * https://stackoverflow.com/help/mcve

    Bug reports which follow these guidelines are easier to diagnose,
    and are often handled much more quickly.

3.  Feature request: do a quick search of existing issues
    to make sure this has not been asked before.

We know asking good questions takes effort, and we appreciate your time.
Thank you.
-->

### What did you do?

Tried to access TIFF tags with the Pillow library but get nothing.  
I can upload my .tiff file to https://exifinfo.org/ and see many tags. 

### What did you expect to happen?

To be able to access exif tags of my tiff files.

### What actually happened?

An empty dict is returned.

### What are your OS, Python and Pillow versions?

* OS: Ubuntu 20.04 (WSL)
* Python: 3.7.9
* Pillow: 7.2.0

<!--
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.

The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as Plone, Django, or Buildout, try to replicate the issue just using Pillow.
-->

```python
from PIL import Image

img = Image.open(file_name)
exif_data = img.getexif()
dict(exif_data)
# returns {}
```



Discussion:
Please provide a copy of your image so that we can replicate the problem.
Thanks for the reply. Here is a zip file of the .tiff file.  

[20191002_151243_137.zip](https://github.com/python-pillow/Pillow/files/5318921/20191002_151243_137.zip)

Depending on what data you're after, you may appreciate this.
```python
from PIL import Image
img = Image.open('20191002_151243_137.TIFF')
print(img.tag_v2)
```
gives
```
{256: 640, 257: 512, 258: (16,), 259: 1, 262: 1, 271: 'FLIR', 272: 'Duo Pro R', 273: (8,), 339: (1,), 277: 1, 278: 512, 279: (655360,), 282: 1.0, 283: 1.0, 284: 1, 296: 1, 297: (0, 1), 50735: '278290', 305: 'V01.02.05', 700: b'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>\r\n<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\r\nxmlns:Camera="http://pix4d.com/camera/1.0/"\r\nxmlns:FLIR="http://ns.flir.com/xmp/1.0/">\r\n<rdf:Description rdf:about="">\r\n<Camera:BandName>\r\n<rdf:Seq>\r\n<rdf:li>LWIR</rdf:li>\r\n</rdf:Seq>\r\n</Camera:BandName>\r\n<Camera:CentralWavelength>\r\n<rdf:Seq>\r\n<rdf:li>10000</rdf:li>\r\n</rdf:Seq>\r\n</Camera:CentralWavelength>\r\n<Camera:WavelengthFWHM>\r\n<rdf:Seq>\r\n<rdf:li>4500</rdf:li>\r\n</rdf:Seq>\r\n</Camera:WavelengthFWHM>\r\n<Camera:TlinearGain>0.04</Camera:TlinearGain>\r\n<Camera:Yaw>-11954/100</Camera:Yaw>\r\n<Camera:Pitch>-8497/100</Camera:Pitch>\r\n<Camera:Roll>9660/100</Camera:Roll>\r\n<Camera:GPSXYAccuracy>1.92</Camera:GPSXYAccuracy>\r\n<Camera:GPSZAccuracy>2.79</Camera:GPSZAccuracy>\r\n<Camera:GyroRate>2.55</Camera:GyroRate>\r\n<Camera:DetectorBitDepth>16</Camera:DetectorBitDepth>\r\n<Camera:IsNormalized>1</Camera:IsNormalized>\r\n<FLIR:ImageOffsetX>0</FLIR:ImageOffsetX>\r\n<FLIR:ImageOffsetY>0</FLIR:ImageOffsetY>\r\n<FLIR:ImageValidStartX>0</FLIR:ImageValidStartX>\r\n<FLIR:ImageValidEndX>639</FLIR:ImageValidEndX>\r\n<FLIR:ImageValidStartY>0</FLIR:ImageValidStartY>\r\n<FLIR:ImageValidEndY>511</FLIR:ImageValidEndY>\r\n<FLIR:ImageUpsampleMode>2</FLIR:ImageUpsampleMode>\r\n<FLIR:MAVVersionID>0.3.0.0</FLIR:MAVVersionID>\r\n<FLIR:MAVComponentID>100</FLIR:MAVComponentID>\r\n<FLIR:MAVRelativeAltitude>0/1000</FLIR:MAVRelativeAltitude>\r\n<FLIR:MAVRateOfClimbRef>M</FLIR:MAVRateOfClimbRef>\r\n<FLIR:MAVRateOfClimb>0/1000</FLIR:MAVRateOfClimb>\r\n<FLIR:MAVYaw>0/100</FLIR:MAVYaw>\r\n<FLIR:MAVPitch>0/100</FLIR:MAVPitch>\r\n<FLIR:MAVRoll>0/100</FLIR:MAVRoll>\r\n<FLIR:MAVYawRate>0/100</FLIR:MAVYawRate>\r\n<FLIR:MAVPitchRate>0/100</FLIR:MAVPitchRate>\r\n<FLIR:MAVRollRate>0/100</FLIR:MAVRollRate>\r\n</rdf:Description>\r\n</rdf:RDF>\r\n<?xpacket end="w"?>\x00'}
```
thanks for your reply!  

yes, I had found that already but i need the exif tags, i.e. GPS information. 

on a related note, I can't add exif tags to a .tiff file either.  I can do so with a .jpg file.

it seems these issues are related to the .tiff file format. 

```python
import piexif
import numpy as np
from PIL import Image
# make a dict of exif tags
exif_bytes = piexif.dump( exif_dict )
# make array of random data
arr = np.random.randint(0, 255, size=[100, 100, 3], dtype=np.uint8)
# use fromarray method to convert to img
img = Image.fromarray(arr)
# add exif bytes to image, 
img.info['exif'] = exif_bytes
# this save it 
img.save(
    'temp.tiff',
    format='tiff',
)
# Alternately, pass the exif_bytes to the save method.
img.save(
    'temp.tiff',
    format='tiff',
    exif=exif_bytes
)
```  

Neither assigning the `exif_bytes` to `info`, nor passing it as an argument to `save()` work for me. 
`Image.open()` does not recognize the exif tags I added.
bump? any ideas here? 
When Pillow saves TIFF images, it does not use 'exif' from the `info` dictionary. Instead, 'tiffinfo' can be used - https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#saving-tiff-images

Looking for the GPS info in your image, Pillow reports that there is a GPS IFD, but that it has type 13. This is unexpected, because [page 16 of the specification](https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf) only lists up to 12. Whatever program has created your image appears to have instead followed [TIFF Specification Supplement 1](https://www.awaresystems.be/imaging/tiff/specification/TIFFPM6.pdf), which specifies type 13 as IFD.

So Pillow doesn't currently support reading the GPS data from your image, and it would need to be added. Would you be able to give permission to add your image to the our test suite, and distribute it under the Pillow license?
Hi! Thanks so much for the response and the info. 

You can definitely use the image in your test suite and license it as such. 

Do you have a timeline for when the package will be updated to work for our images? 
Since you're interested in an immediate solution, does this suit your needs?

```python
from PIL import Image, TiffTags
from PIL.TiffImagePlugin import ImageFileDirectory_v2

# Add IFD as a tag type
ImageFileDirectory_v2._load_dispatch[13] = ImageFileDirectory_v2._load_dispatch[TiffTags.LONG]

im = Image.open('20191002_151243_137.TIFF')
gps_offset = im.tag_v2.get(0x8825)
if gps_offset:
	ifh = b"II\x2A\x00\x08\x00\x00\x00" if im.tag_v2._endian == "<" else "MM\x00\x2A\x00\x00\x00\x08"
	info = ImageFileDirectory_v2(ifh)
	im.fp.seek(gps_offset)
	info.load(im.fp)
	
	data = {}
	gps_keys = ['GPSVersionID','GPSLatitudeRef','GPSLatitude','GPSLongitudeRef','GPSLongitude','GPSAltitudeRef','GPSAltitude','GPSTimeStamp','GPSSatellites','GPSStatus','GPSMeasureMode','GPSDOP','GPSSpeedRef','GPSSpeed','GPSTrackRef','GPSTrack','GPSImgDirectionRef','GPSImgDirection','GPSMapDatum','GPSDestLatitudeRef','GPSDestLatitude','GPSDestLongitudeRef','GPSDestLongitude','GPSDestBearingRef','GPSDestBearing','GPSDestDistanceRef','GPSDestDistance','GPSProcessingMethod','GPSAreaInformation','GPSDateStamp','GPSDifferential']
	for k, v in info.items():
		print(gps_keys[k]+": "+str(v))
else:
	print("No GPS data present")
```

Running it over your image, I get
```
GPSVersionID: b'\x03\x02\x00\x00'
GPSLatitudeRef: N
GPSLatitude: (35.0, 35.0, 59.995)
GPSLongitudeRef: W
GPSLongitude: (111.0, 39.0, 42.578)
GPSAltitudeRef: b'\x00'
GPSAltitude: 1845.656
GPSMapDatum: WGS-84
```
This is now marginally simpler with Pillow 8.0, thanks to #4979

```python
from PIL import Image, TiffTags
from PIL.TiffImagePlugin import ImageFileDirectory_v2

im = Image.open('20191002_151243_137.TIFF')
gps_offset = im.tag_v2.get(0x8825)
if gps_offset:
	ifh = b"II\x2A\x00\x08\x00\x00\x00" if im.tag_v2._endian == "<" else "MM\x00\x2A\x00\x00\x00\x08"
	info = ImageFileDirectory_v2(ifh)
	im.fp.seek(gps_offset)
	info.load(im.fp)
	
	data = {}
	gps_keys = ['GPSVersionID','GPSLatitudeRef','GPSLatitude','GPSLongitudeRef','GPSLongitude','GPSAltitudeRef','GPSAltitude','GPSTimeStamp','GPSSatellites','GPSStatus','GPSMeasureMode','GPSDOP','GPSSpeedRef','GPSSpeed','GPSTrackRef','GPSTrack','GPSImgDirectionRef','GPSImgDirection','GPSMapDatum','GPSDestLatitudeRef','GPSDestLatitude','GPSDestLongitudeRef','GPSDestLongitude','GPSDestBearingRef','GPSDestBearing','GPSDestDistanceRef','GPSDestDistance','GPSProcessingMethod','GPSAreaInformation','GPSDateStamp','GPSDifferential']
	for k, v in info.items():
		print(gps_keys[k]+": "+str(v))
else:
	print("No GPS data present")
```
With #5416, this is now as simple as
```python
from PIL import Image
img = Image.open('20191002_151243_137.TIFF')
info = img.getexif().get_ifd(0x8825)
if info:
	gps_keys = ['GPSVersionID','GPSLatitudeRef','GPSLatitude','GPSLongitudeRef','GPSLongitude','GPSAltitudeRef','GPSAltitude','GPSTimeStamp','GPSSatellites','GPSStatus','GPSMeasureMode','GPSDOP','GPSSpeedRef','GPSSpeed','GPSTrackRef','GPSTrack','GPSImgDirectionRef','GPSImgDirection','GPSMapDatum','GPSDestLatitudeRef','GPSDestLatitude','GPSDestLongitudeRef','GPSDestLongitude','GPSDestBearingRef','GPSDestBearing','GPSDestDistanceRef','GPSDestDistance','GPSProcessingMethod','GPSAreaInformation','GPSDateStamp','GPSDifferential']
	for k, v in info.items():
		print(gps_keys[k]+": "+str(v))
else:
	print("No GPS data present")
```

Resolving patch:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1143,6 +1143,17 @@ def load_end(self):
         if not self.is_animated:
             self._close_exclusive_fp_after_loading = True
 
+            # reset buffered io handle in case fp
+            # was passed to libtiff, invalidating the buffer
+            self.fp.tell()
+
+            # load IFD data from fp before it is closed
+            exif = self.getexif()
+            for key in TiffTags.TAGS_V2_GROUPS.keys():
+                if key not in exif:
+                    continue
+                exif.get_ifd(key)
+
     def _load_libtiff(self):
         """Overload method triggered when we detect a compressed tiff
         Calls out to libtiff"""
@@ -1507,12 +1518,24 @@ def _save(im, fp, filename):
     ifd[IMAGELENGTH] = im.size[1]
 
     # write any arbitrary tags passed in as an ImageFileDirectory
-    info = encoderinfo.get("tiffinfo", {})
+    if "tiffinfo" in encoderinfo:
+        info = encoderinfo["tiffinfo"]
+    elif "exif" in encoderinfo:
+        info = encoderinfo["exif"]
+        if isinstance(info, bytes):
+            exif = Image.Exif()
+            exif.load(info)
+            info = exif
+    else:
+        info = {}
     logger.debug("Tiffinfo Keys: %s" % list(info))
     if isinstance(info, ImageFileDirectory_v1):
         info = info.to_v2()
     for key in info:
-        ifd[key] = info.get(key)
+        if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS.keys():
+            ifd[key] = info.get_ifd(key)
+        else:
+            ifd[key] = info.get(key)
         try:
             ifd.tagtype[key] = info.tagtype[key]
         except Exception:


Test patch:
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -403,10 +403,8 @@ def test_ifd_tag_type(self):
         with Image.open("Tests/images/ifd_tag_type.tiff") as im:
             assert 0x8825 in im.tag_v2
 
-    def test_exif(self):
-        with Image.open("Tests/images/ifd_tag_type.tiff") as im:
-            exif = im.getexif()
-
+    def test_exif(self, tmp_path):
+        def check_exif(exif):
             assert sorted(exif.keys()) == [
                 256,
                 257,
@@ -439,6 +437,24 @@ def test_exif(self):
             assert gps[0] == b"\x03\x02\x00\x00"
             assert gps[18] == "WGS-84"
 
+        outfile = str(tmp_path / "temp.tif")
+        with Image.open("Tests/images/ifd_tag_type.tiff") as im:
+            exif = im.getexif()
+            check_exif(exif)
+
+            im.save(outfile, exif=exif)
+
+        outfile2 = str(tmp_path / "temp2.tif")
+        with Image.open(outfile) as im:
+            exif = im.getexif()
+            check_exif(exif)
+
+            im.save(outfile2, exif=exif.tobytes())
+
+        with Image.open(outfile2) as im:
+            exif = im.getexif()
+            check_exif(exif)
+
     def test_exif_frames(self):
         # Test that EXIF data can change across frames
         with Image.open("Tests/images/g4-multi.tiff") as im: