Moved ImageFile.Exif to Image.Exif

This commit is contained in:
Andrew Murray 2019-04-01 20:03:02 +11:00
parent 8f0db65cd7
commit 40bc46ff49
7 changed files with 214 additions and 228 deletions

View File

@ -1,7 +1,6 @@
from .helper import PillowTestCase, hopper from .helper import PillowTestCase, hopper
from PIL import Image from PIL import Image
from PIL import ImageFile
from PIL import ImageOps from PIL import ImageOps
try: try:
@ -256,9 +255,7 @@ class TestImageOps(PillowTestCase):
else: else:
self.assertNotEqual(transposed_im.info["exif"], original_exif) self.assertNotEqual(transposed_im.info["exif"], original_exif)
exif = ImageFile.Exif() self.assertNotIn(0x0112, transposed_im.getexif())
exif.load(transposed_im.info["exif"])
self.assertNotIn(0x0112, exif)
# Repeat the operation, to test that it does not keep transposing # Repeat the operation, to test that it does not keep transposing
transposed_im2 = ImageOps.exif_transpose(transposed_im) transposed_im2 = ImageOps.exif_transpose(transposed_im)

View File

@ -40,8 +40,8 @@ except ImportError:
import __builtin__ import __builtin__
builtins = __builtin__ builtins = __builtin__
from . import ImageMode from . import ImageMode, TiffTags
from ._binary import i8 from ._binary import i8, i32le
from ._util import isPath, isStringType, deferred_error from ._util import isPath, isStringType, deferred_error
import os import os
@ -54,10 +54,10 @@ import atexit
import numbers import numbers
try: try:
# Python 3 # Python 3
from collections.abc import Callable from collections.abc import Callable, MutableMapping
except ImportError: except ImportError:
# Python 2.7 # Python 2.7
from collections import Callable from collections import Callable, MutableMapping
# Silence warning # Silence warning
@ -1297,6 +1297,12 @@ class Image(object):
return tuple(extrema) return tuple(extrema)
return self.im.getextrema() return self.im.getextrema()
def getexif(self):
exif = Exif()
if "exif" in self.info:
exif.load(self.info["exif"])
return exif
def getim(self): def getim(self):
""" """
Returns a capsule that points to the internal image memory. Returns a capsule that points to the internal image memory.
@ -3005,3 +3011,182 @@ def _apply_env_variables(env=None):
_apply_env_variables() _apply_env_variables()
atexit.register(core.clear_cache) 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))

View File

@ -27,20 +27,11 @@
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
from . import Image, TiffTags from . import Image
from ._binary import i32le from ._util import isPath
from ._util import isPath, py3
import io import io
import sys import sys
import struct import struct
import warnings
try:
# Python 3
from collections.abc import MutableMapping
except ImportError:
# Python 2.7
from collections import MutableMapping
MAXBLOCK = 65536 MAXBLOCK = 65536
@ -302,12 +293,6 @@ class ImageFile(Image.Image):
return self.tell() != frame return self.tell() != frame
def getexif(self):
exif = Exif()
if "exif" in self.info:
exif.load(self.info["exif"])
return exif
class StubImageFile(ImageFile): class StubImageFile(ImageFile):
""" """
@ -687,182 +672,3 @@ class PyDecoder(object):
raise ValueError("not enough image data") raise ValueError("not enough image data")
if s[1] != 0: if s[1] != 0:
raise ValueError("cannot decode image data") raise ValueError("cannot decode image data")
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))

View File

@ -17,7 +17,7 @@
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
from . import Image, ImageFile from . import Image
from ._util import isStringType from ._util import isStringType
import operator import operator
import functools import functools
@ -532,22 +532,20 @@ def exif_transpose(image):
:param image: The image to transpose. :param image: The image to transpose.
:return: An image. :return: An image.
""" """
if "exif" in image.info: exif = image.getexif()
exif = ImageFile.Exif() orientation = exif.get(0x0112)
exif.load(image.info["exif"]) method = {
orientation = exif.get(0x0112) 2: Image.FLIP_LEFT_RIGHT,
method = { 3: Image.ROTATE_180,
2: Image.FLIP_LEFT_RIGHT, 4: Image.FLIP_TOP_BOTTOM,
3: Image.ROTATE_180, 5: Image.TRANSPOSE,
4: Image.FLIP_TOP_BOTTOM, 6: Image.ROTATE_270,
5: Image.TRANSPOSE, 7: Image.TRANSVERSE,
6: Image.ROTATE_270, 8: Image.ROTATE_90
7: Image.TRANSVERSE, }.get(orientation)
8: Image.ROTATE_90 if method is not None:
}.get(orientation) transposed_image = image.transpose(method)
if method is not None: del exif[0x0112]
transposed_image = image.transpose(method) transposed_image.info["exif"] = exif.tobytes()
del exif[0x0112] return transposed_image
transposed_image.info["exif"] = exif.tobytes()
return transposed_image
return image.copy() return image.copy()

View File

@ -472,7 +472,7 @@ class JpegImageFile(ImageFile.ImageFile):
def _fixup_dict(src_dict): def _fixup_dict(src_dict):
# Helper function for _getexif() # Helper function for _getexif()
# returns a dict with any single item tuples/lists as individual values # returns a dict with any single item tuples/lists as individual values
exif = ImageFile.Exif() exif = Image.Exif()
return exif._fixup_dict(src_dict) return exif._fixup_dict(src_dict)
@ -725,7 +725,7 @@ def _save(im, fp, filename):
optimize = info.get("optimize", False) optimize = info.get("optimize", False)
exif = info.get("exif", b"") exif = info.get("exif", b"")
if isinstance(exif, ImageFile.Exif): if isinstance(exif, Image.Exif):
exif = exif.tobytes() exif = exif.tobytes()
# get keyword arguments # get keyword arguments

View File

@ -886,7 +886,7 @@ def _save(im, fp, filename, chunk=putchunk):
exif = im.encoderinfo.get("exif", im.info.get("exif")) exif = im.encoderinfo.get("exif", im.info.get("exif"))
if exif: if exif:
if isinstance(exif, ImageFile.Exif): if isinstance(exif, Image.Exif):
exif = exif.tobytes(8) exif = exif.tobytes(8)
if exif.startswith(b"Exif\x00\x00"): if exif.startswith(b"Exif\x00\x00"):
exif = exif[6:] exif = exif[6:]

View File

@ -217,7 +217,7 @@ def _save_all(im, fp, filename):
method = im.encoderinfo.get("method", 0) method = im.encoderinfo.get("method", 0)
icc_profile = im.encoderinfo.get("icc_profile", "") icc_profile = im.encoderinfo.get("icc_profile", "")
exif = im.encoderinfo.get("exif", "") exif = im.encoderinfo.get("exif", "")
if isinstance(exif, ImageFile.Exif): if isinstance(exif, Image.Exif):
exif = exif.tobytes() exif = exif.tobytes()
xmp = im.encoderinfo.get("xmp", "") xmp = im.encoderinfo.get("xmp", "")
if allow_mixed: if allow_mixed:
@ -318,7 +318,7 @@ def _save(im, fp, filename):
quality = im.encoderinfo.get("quality", 80) quality = im.encoderinfo.get("quality", 80)
icc_profile = im.encoderinfo.get("icc_profile", "") icc_profile = im.encoderinfo.get("icc_profile", "")
exif = im.encoderinfo.get("exif", "") exif = im.encoderinfo.get("exif", "")
if isinstance(exif, ImageFile.Exif): if isinstance(exif, Image.Exif):
exif = exif.tobytes() exif = exif.tobytes()
xmp = im.encoderinfo.get("xmp", "") xmp = im.encoderinfo.get("xmp", "")