diff --git a/Tests/images/exif.png b/Tests/images/exif.png new file mode 100644 index 000000000..0388b6b8a 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 index 2b80bf357..c2864d223 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -590,6 +590,40 @@ class TestFilePng(PillowTestCase): 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): diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 8f53291c3..663b740d5 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -462,6 +462,10 @@ PNG Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``P``, ``RGB``, or ``RGBA`` data. Interlaced files are supported as of v1.1.7. +As of Pillow 6.0, EXIF data can be read from PNG images. However, unlike other +image formats, EXIF data is not guaranteed to have been read until +:py:meth:`~PIL.Image.Image.load` has been called. + The :py:meth:`~PIL.Image.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties, when appropriate: @@ -527,6 +531,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **icc_profile** The ICC Profile to include in the saved file. +**exif** + The exif data to include in the saved file. + + .. versionadded:: 6.0.0 + **bits (experimental)** For ``P`` images, this option controls how many bits to store. If omitted, the PNG writer uses 8 bits (256 colors). diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst index 09b96939c..58586f3bb 100644 --- a/docs/releasenotes/6.0.0.rst +++ b/docs/releasenotes/6.0.0.rst @@ -112,6 +112,13 @@ Image.quantize The `dither` option is now a customisable parameter (was previously hardcoded to `1`). This parameter takes the same values used in `Image.convert` +PNG EXIF Data +^^^^^^^^^^^^^ + +EXIF data can now be read from and saved to PNG images. However, unlike other image +formats, EXIF data is not guaranteed to have been read until +:py:meth:`~PIL.Image.Image.load` has been called. + Other Changes ============= diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index e04ae2274..df632af1d 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -529,6 +529,11 @@ class PngStream(ChunkStream): 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 @@ class PngImageFile(ImageFile.ImageFile): 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)])