Repository: python-pillow/Pillow

Issue:
PIL failed to extract EXIF while MacOS Preview App can
### What did you do?
Hi, I'm trying to extract EXIF from the bellow PNG image, mainly to rotate it correctly based on the orientation information inside the EXIF tags.

![test](https://user-images.githubusercontent.com/26834698/78507395-ebf37200-77a9-11ea-951d-e60d3560e8ba.png)

with this simple script:

```python
import PIL

print(PIL.__version__)
from PIL import Image

image = 'test.png'
exif_info = Image.open(image).getexif()
print(exif_info)
```

but PIL failed to extract any EXIF information out of it. I can confirm that the EXIF information is there and can be detected via Preview app on MacOS:

![image](https://user-images.githubusercontent.com/26834698/78507416-0fb6b800-77aa-11ea-8670-d80f9d914457.png)

As you can see, it said: **Orientation: 8 (Rotated 90° CW)**

### What did you expect to happen?
`getexif()` return the EXIF information for the image, at least contain the orientation like MacOS Preview did.

Noted that some month ago I ran the same exact code and everything work fine, maybe somehow `getexif()` was broken in newer version of Pillow. I have tried to revert back to `6.0.0` but it does not solve the problem.

Also I have noticed that `getexif()` will almost fail with any file was rotated by Quick Look tool from MacOS

![image](https://user-images.githubusercontent.com/26834698/78507583-1134b000-77ab-11ea-8b1b-d83fe7508846.png)


### What actually happened?
`getexif()` return an empty `dict` and failed to extract any EXIF

Output:
```bash
7.1.1
{}
```

### What are your OS, Python and Pillow versions?

* OS: MacOS Mojave 10.14.5
* Python: Python 3.7.6
* Pillow: 7.1.1


Discussion:
The difference with this image is that it is using XMP TIFF tags to specify the orientation - https://exiftool.org/TagNames/XMP.html#tiff

```python
>>> from PIL import Image
>>> im = Image.open('input.png')
>>> print(im.info)
{'XML:com.adobe.xmp': '<?xpacket begin="\ufeff" id="W5M0MpCehiHzreSzNTczkc9d"?>\n<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">\n <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n  <rdf:Description rdf:about=""\n    xmlns:tiff="http://ns.adobe.com/tiff/1.0/"\n   tiff:Orientation="8"/>\n </rdf:RDF>\n</x:xmpmeta>\n<?xpacket end="r"?>', 'chromaticity': (0.3127, 0.329, 0.64, 0.33, 0.3, 0.6, 0.15, 0.06), 'gamma': 0.45455}
>>> if 'XML:com.adobe.xmp' in im.info and 'tiff:Orientation="' in im.info['XML:com.adobe.xmp']:
...     orientation = int(im.info['XML:com.adobe.xmp'].split('tiff:Orientation="')[1].split('"')[0])
...     print(orientation)
... 
8
```

I would doubt that you ran this code over that image a month ago and it worked. I would suspect that you ran the same code over a different image, one that specified orientation in a different way.

I tried rotating an image with Quick Look and found that Pillow could correctly read the orientation data, so if you'd like that looked at further, please provide a sample image.
I don't suppose you have another image with similar orientation data, that could be added as a test image under the Pillow license?
Thanks for your help @radarhere. I have tried your script and can get the EXIF as you did.

Rotating image with Quick Look also work by reading XMP TIFF tags like you did. But I was wondering that should `.getexif()` work with XMP TIFF as default?

I can help provide sample image, even a PR if it's not too difficult
I think that at least [`exif_transpose()`](https://pillow.readthedocs.io/en/stable/reference/ImageOps.html#PIL.ImageOps.exif_transpose) should work with it. I'm not sure exactly about how `getexif()` should handle it.

But yes, you're free to create a PR with that if you want, or just provide a sample image.
@radarhere For me though, 'exif_transpose()` does not work with some other images ;_; that's why I always try to get the exif tag and rotate it myself.

I'm not sure where to start yet since I'm not that familiar with PIL codebase but anyways, you can use all the images in above comment as a sample
I'm interested an image that could be included in our test suite under the Pillow license. I don't think you have authority to give permission for us to use the image of baby Yoda.
Oh yeah, I agree @radarhere. How about this ? 
![test](https://user-images.githubusercontent.com/26834698/79414488-f6491380-7fd4-11ea-960b-daee8c1ec36e.png)
I cropped this from one of the photo that I took and it is using XMP TIFF tags.

Resolving patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -35,6 +35,7 @@
 import sys
 import tempfile
 import warnings
+import xml.etree.ElementTree
 from collections.abc import Callable, MutableMapping
 from pathlib import Path
 
@@ -1300,7 +1301,28 @@ def getextrema(self):
     def getexif(self):
         if self._exif is None:
             self._exif = Exif()
-        self._exif.load(self.info.get("exif"))
+
+        exif_info = self.info.get("exif")
+        if exif_info is None and "Raw profile type exif" in self.info:
+            exif_info = bytes.fromhex(
+                "".join(self.info["Raw profile type exif"].split("\n")[3:])
+            )
+        self._exif.load(exif_info)
+
+        # XMP tags
+        if 0x0112 not in self._exif:
+            xmp_tags = self.info.get("XML:com.adobe.xmp")
+            if xmp_tags:
+                root = xml.etree.ElementTree.fromstring(xmp_tags)
+                for elem in root.iter():
+                    if elem.tag.endswith("}Description"):
+                        orientation = elem.attrib.get(
+                            "{http://ns.adobe.com/tiff/1.0/}Orientation"
+                        )
+                        if orientation:
+                            self._exif[0x0112] = int(orientation)
+                        break
+
         return self._exif
 
     def getim(self):
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -931,16 +931,7 @@ def getexif(self):
         if "exif" not in self.info:
             self.load()
 
-        if self._exif is None:
-            self._exif = Image.Exif()
-
-        exif_info = self.info.get("exif")
-        if exif_info is None and "Raw profile type exif" in self.info:
-            exif_info = bytes.fromhex(
-                "".join(self.info["Raw profile type exif"].split("\n")[3:])
-            )
-        self._exif.load(exif_info)
-        return self._exif
+        return super().getexif()
 
     def _close__fp(self):
         try:


Test patch:
diff --git a/Tests/images/xmp_tags_orientation.png b/Tests/images/xmp_tags_orientation.png
new file mode 100644
Binary files /dev/null and b/Tests/images/xmp_tags_orientation.png differ
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -591,18 +591,27 @@ def test_textual_chunks_after_idat(self):
         with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
             assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
 
-    @pytest.mark.parametrize(
-        "test_file",
-        [
-            "Tests/images/exif.png",  # With an EXIF chunk
-            "Tests/images/exif_imagemagick.png",  # With an ImageMagick zTXt chunk
-        ],
-    )
-    def test_exif(self, test_file):
-        with Image.open(test_file) as im:
+    def test_exif(self):
+        # With an EXIF chunk
+        with Image.open("Tests/images/exif.png") as im:
             exif = im._getexif()
         assert exif[274] == 1
 
+        # With an ImageMagick zTXt chunk
+        with Image.open("Tests/images/exif_imagemagick.png") as im:
+            exif = im._getexif()
+            assert exif[274] == 1
+
+            # Assert that info still can be extracted
+            # when the image is no longer a PngImageFile instance
+            exif = im.copy().getexif()
+            assert exif[274] == 1
+
+        # With XMP tags
+        with Image.open("Tests/images/xmp_tags_orientation.png") as im:
+            exif = im.getexif()
+        assert exif[274] == 3
+
     def test_exif_save(self, tmp_path):
         with Image.open("Tests/images/exif.png") as im:
             test_file = str(tmp_path / "temp.png")