Repository: python-pillow/Pillow

Issue:
Support for EXIF in PNG format
Based on http://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html#C.eXIf, ~it looks like there is a push to officially add exif support into the PNG image format~, it looks like they have defined how to include EXIF data in a PNG 'data chunk' (a standard mechanism 'adding extra data' to PNG files).

Given the work EXIF-related started in #3625 (which addresses #520), is it reasonable to consider this feature request for Pillow?


Discussion:
Yes, it is reasonable. However, by 'there is a push to officially add', do you mean that it is not yet officially supported?
That was poor wording on my part, I think. The PNG format 'extension' seems to be legitimate, and official. The PNG format isn't modified at all, but the 'extension' merely defines how EXIF should be included as a 'data chunk', which the format already supports. 

I'll edit the original description above.

Resolving patch:
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -529,6 +529,11 @@ def chunk_iTXt(self, pos, length):
 
         return s
 
+    def chunk_eXIf(self, pos, length):
+        s = ImageFile._safe_read(self.fp, length)
+        self.im_info["exif"] = b"Exif\x00\x00"+s
+        return s
+
     # APNG chunks
     def chunk_acTL(self, pos, length):
         s = ImageFile._safe_read(self.fp, length)
@@ -683,6 +688,12 @@ def load_end(self):
         self.png.close()
         self.png = None
 
+    def _getexif(self):
+        if "exif" not in self.info:
+            self.load()
+        from .JpegImagePlugin import _getexif
+        return _getexif(self)
+
 
 # --------------------------------------------------------------------
 # PNG writer
@@ -861,6 +872,12 @@ def _save(im, fp, filename, chunk=putchunk):
                 chunks.remove(cid)
                 chunk(fp, cid, data)
 
+    exif = im.encoderinfo.get("exif", im.info.get("exif"))
+    if exif:
+        if exif.startswith(b"Exif\x00\x00"):
+            exif = exif[6:]
+        chunk(fp, b"eXIf", exif)
+
     ImageFile._save(im, _idat(fp, chunk),
                     [("zip", (0, 0)+im.size, 0, rawmode)])
 


Test patch:
diff --git a/Tests/images/exif.png b/Tests/images/exif.png
new file mode 100644
Binary files /dev/null and b/Tests/images/exif.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
@@ -590,6 +590,40 @@ def test_textual_chunks_after_idat(self):
         im = Image.open("Tests/images/hopper_idat_after_image_end.png")
         self.assertEqual(im.text, {'TXT': 'VALUE', 'ZIP': 'VALUE'})
 
+    def test_exif(self):
+        im = Image.open("Tests/images/exif.png")
+        exif = im._getexif()
+        self.assertEqual(exif[274], 1)
+
+    def test_exif_save(self):
+        im = Image.open("Tests/images/exif.png")
+
+        test_file = self.tempfile("temp.png")
+        im.save(test_file)
+
+        reloaded = Image.open(test_file)
+        exif = reloaded._getexif()
+        self.assertEqual(exif[274], 1)
+
+    def test_exif_from_jpg(self):
+        im = Image.open("Tests/images/pil_sample_rgb.jpg")
+
+        test_file = self.tempfile("temp.png")
+        im.save(test_file)
+
+        reloaded = Image.open(test_file)
+        exif = reloaded._getexif()
+        self.assertEqual(exif[305], "Adobe Photoshop CS Macintosh")
+
+    def test_exif_argument(self):
+        im = Image.open(TEST_PNG_FILE)
+
+        test_file = self.tempfile("temp.png")
+        im.save(test_file, exif=b"exifstring")
+
+        reloaded = Image.open(test_file)
+        self.assertEqual(reloaded.info["exif"], b"Exif\x00\x00exifstring")
+
     @unittest.skipUnless(HAVE_WEBP and _webp.HAVE_WEBPANIM,
                          "WebP support not installed with animation")
     def test_apng(self):