Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions Tests/test_imageops.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .helper import PillowTestCase, hopper

from PIL import ImageOps
from PIL import Image
from PIL import ImageOps

try:
from PIL import _webp
Expand Down Expand Up @@ -239,10 +239,24 @@ def test_exif_transpose(self):
for i in range(2, 9):
im = Image.open("Tests/images/hopper_orientation_"+str(i)+ext)
orientations.append(im)
for im in orientations:
transposed_im = ImageOps.exif_transpose(im)
self.assert_image_similar(base_im, transposed_im, 17)

# Repeat the operation, to test that it does not keep transposing
transposed_im2 = ImageOps.exif_transpose(transposed_im)
self.assert_image_equal(transposed_im2, transposed_im)
for i, orientation_im in enumerate(orientations):
for im in [
orientation_im, # ImageFile
orientation_im.copy() # Image
]:
if i == 0:
self.assertNotIn("exif", im.info)
else:
original_exif = im.info["exif"]
transposed_im = ImageOps.exif_transpose(im)
self.assert_image_similar(base_im, transposed_im, 17)
if i == 0:
self.assertNotIn("exif", im.info)
else:
self.assertNotEqual(transposed_im.info["exif"], original_exif)

self.assertNotIn(0x0112, transposed_im.getexif())

# Repeat the operation, to test that it does not keep transposing
transposed_im2 = ImageOps.exif_transpose(transposed_im)
self.assert_image_equal(transposed_im2, transposed_im)
5 changes: 3 additions & 2 deletions docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,9 @@ Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``LA``,
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.
image formats, EXIF data is not guaranteed to be present in
:py:attr:`~PIL.Image.Image.info` 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:
Expand Down
12 changes: 10 additions & 2 deletions docs/releasenotes/6.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,20 @@ language-specific glyphs and ligatures from the font:
* ``ImageFont.ImageFont.getsize_multiline()``
* ``ImageFont.ImageFont.getsize()``

Added EXIF class
^^^^^^^^^^^^^^^^

:py:meth:`~PIL.Image.Image.getexif` has been added, and returning an
:py:class:`~PIL.Image.Exif` instance. Values can be retrieved and set like a
dictionary. When saving JPEG, PNG or WEBP, the instance can be passed as an
``exif`` argument to include any changes in the output image.

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.
formats, EXIF data is not guaranteed to be present in :py:attr:`~PIL.Image.Image.info`
until :py:meth:`~PIL.Image.Image.load` has been called.

Other Changes
=============
Expand Down
193 changes: 189 additions & 4 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
import __builtin__
builtins = __builtin__

from . import ImageMode
from ._binary import i8
from . import ImageMode, TiffTags
from ._binary import i8, i32le
from ._util import isPath, isStringType, deferred_error

import os
Expand All @@ -54,10 +54,10 @@
import numbers
try:
# Python 3
from collections.abc import Callable
from collections.abc import Callable, MutableMapping
except ImportError:
# Python 2.7
from collections import Callable
from collections import Callable, MutableMapping


# Silence warning
Expand Down Expand Up @@ -1297,6 +1297,12 @@ def getextrema(self):
return tuple(extrema)
return self.im.getextrema()

def getexif(self):
exif = Exif()
if "exif" in self.info:
exif.load(self.info["exif"])
return exif

def getim(self):
"""
Returns a capsule that points to the internal image memory.
Expand Down Expand Up @@ -3005,3 +3011,182 @@ def _apply_env_variables(env=None):

_apply_env_variables()
atexit.register(core.clear_cache)


class Exif(MutableMapping):
endian = "<"

def __init__(self):
self._data = {}
self._ifds = {}

def _fixup_dict(self, src_dict):
# Helper function for _getexif()
# returns a dict with any single item tuples/lists as individual values
def _fixup(value):
try:
if len(value) == 1 and not isinstance(value, dict):
return value[0]
except Exception:
pass
return value

return {k: _fixup(v) for k, v in src_dict.items()}

def _get_ifd_dict(self, tag):
try:
# an offset pointer to the location of the nested embedded IFD.
# It should be a long, but may be corrupted.
self.fp.seek(self._data[tag])
except (KeyError, TypeError):
pass
else:
from . import TiffImagePlugin
info = TiffImagePlugin.ImageFileDirectory_v1(self.head)
info.load(self.fp)
return self._fixup_dict(info)

def load(self, data):
# Extract EXIF information. This is highly experimental,
# and is likely to be replaced with something better in a future
# version.

# The EXIF record consists of a TIFF file embedded in a JPEG
# application marker (!).
self.fp = io.BytesIO(data[6:])
self.head = self.fp.read(8)
# process dictionary
from . import TiffImagePlugin
info = TiffImagePlugin.ImageFileDirectory_v1(self.head)
self.endian = info._endian
self.fp.seek(info.next)
info.load(self.fp)
self._data = dict(self._fixup_dict(info))

# get EXIF extension
ifd = self._get_ifd_dict(0x8769)
if ifd:
self._data.update(ifd)
self._ifds[0x8769] = ifd

# get gpsinfo extension
ifd = self._get_ifd_dict(0x8825)
if ifd:
self._data[0x8825] = ifd
self._ifds[0x8825] = ifd

def tobytes(self, offset=0):
from . import TiffImagePlugin
if self.endian == "<":
head = b"II\x2A\x00\x08\x00\x00\x00"
else:
head = b"MM\x00\x2A\x00\x00\x00\x08"
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
for tag, value in self._data.items():
ifd[tag] = value
return b"Exif\x00\x00"+head+ifd.tobytes(offset)

def get_ifd(self, tag):
if tag not in self._ifds and tag in self._data:
if tag == 0xa005: # interop
self._ifds[tag] = self._get_ifd_dict(tag)
elif tag == 0x927c: # makernote
from . import TiffImagePlugin
if self._data[0x927c][:8] == b"FUJIFILM":
exif_data = self._data[0x927c]
ifd_offset = i32le(exif_data[8:12])
ifd_data = exif_data[ifd_offset:]

makernote = {}
for i in range(0, struct.unpack("<H", ifd_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
"<HHL4s", ifd_data[i*12 + 2:(i+1)*12 + 2])
try:
unit_size, handler =\
TiffImagePlugin.ImageFileDirectory_v2._load_dispatch[
typ
]
except KeyError:
continue
size = count * unit_size
if size > 4:
offset, = struct.unpack("<L", data)
data = ifd_data[offset-12:offset+size-12]
else:
data = data[:size]

if len(data) != size:
warnings.warn("Possibly corrupt EXIF MakerNote data. "
"Expecting to read %d bytes but only got %d."
" Skipping tag %s"
% (size, len(data), ifd_tag))
continue

if not data:
continue

makernote[ifd_tag] = handler(
TiffImagePlugin.ImageFileDirectory_v2(), data, False)
self._ifds[0x927c] = dict(self._fixup_dict(makernote))
elif self._data.get(0x010f) == "Nintendo":
ifd_data = self._data[0x927c]

makernote = {}
for i in range(0, struct.unpack(">H", ifd_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
">HHL4s", ifd_data[i*12 + 2:(i+1)*12 + 2])
if ifd_tag == 0x1101:
# CameraInfo
offset, = struct.unpack(">L", data)
self.fp.seek(offset)

camerainfo = {'ModelID': self.fp.read(4)}

self.fp.read(4)
# Seconds since 2000
camerainfo['TimeStamp'] = i32le(self.fp.read(12))

self.fp.read(4)
camerainfo['InternalSerialNumber'] = self.fp.read(4)

self.fp.read(12)
parallax = self.fp.read(4)
handler =\
TiffImagePlugin.ImageFileDirectory_v2._load_dispatch[
TiffTags.FLOAT
][1]
camerainfo['Parallax'] = handler(
TiffImagePlugin.ImageFileDirectory_v2(),
parallax, False)

self.fp.read(4)
camerainfo['Category'] = self.fp.read(2)

makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
self._ifds[0x927c] = makernote
return self._ifds.get(tag, {})

def __str__(self):
return str(self._data)

def __len__(self):
return len(self._data)

def __getitem__(self, tag):
return self._data[tag]

def __contains__(self, tag):
return tag in self._data

if not py3:
def has_key(self, tag):
return tag in self

def __setitem__(self, tag, value):
self._data[tag] = value

def __delitem__(self, tag):
del self._data[tag]

def __iter__(self):
return iter(set(self._data))
Loading