diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 3b3a63122..eaa8fde3c 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,7 +1,6 @@ from .helper import PillowTestCase, hopper from PIL import Image -from PIL import ImageFile from PIL import ImageOps try: @@ -256,9 +255,7 @@ class TestImageOps(PillowTestCase): else: self.assertNotEqual(transposed_im.info["exif"], original_exif) - exif = ImageFile.Exif() - exif.load(transposed_im.info["exif"]) - self.assertNotIn(0x0112, 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) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a43ec9814..18121c6fe 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -40,8 +40,8 @@ except ImportError: 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 @@ -54,10 +54,10 @@ import atexit 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 @@ -1297,6 +1297,12 @@ class Image(object): 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. @@ -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(" 4: + offset, = 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)) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 99cb7ec37..06e7b76ba 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -27,20 +27,11 @@ # See the README file for information on usage and redistribution. # -from . import Image, TiffTags -from ._binary import i32le -from ._util import isPath, py3 +from . import Image +from ._util import isPath import io import sys import struct -import warnings - -try: - # Python 3 - from collections.abc import MutableMapping -except ImportError: - # Python 2.7 - from collections import MutableMapping MAXBLOCK = 65536 @@ -302,12 +293,6 @@ class ImageFile(Image.Image): return self.tell() != frame - def getexif(self): - exif = Exif() - if "exif" in self.info: - exif.load(self.info["exif"]) - return exif - class StubImageFile(ImageFile): """ @@ -687,182 +672,3 @@ class PyDecoder(object): raise ValueError("not enough image data") if s[1] != 0: 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(" 4: - offset, = 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)) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 2cab10884..ab6a3d2c4 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -17,7 +17,7 @@ # See the README file for information on usage and redistribution. # -from . import Image, ImageFile +from . import Image from ._util import isStringType import operator import functools @@ -532,22 +532,20 @@ def exif_transpose(image): :param image: The image to transpose. :return: An image. """ - if "exif" in image.info: - exif = ImageFile.Exif() - exif.load(image.info["exif"]) - orientation = exif.get(0x0112) - method = { - 2: Image.FLIP_LEFT_RIGHT, - 3: Image.ROTATE_180, - 4: Image.FLIP_TOP_BOTTOM, - 5: Image.TRANSPOSE, - 6: Image.ROTATE_270, - 7: Image.TRANSVERSE, - 8: Image.ROTATE_90 - }.get(orientation) - if method is not None: - transposed_image = image.transpose(method) - del exif[0x0112] - transposed_image.info["exif"] = exif.tobytes() - return transposed_image + exif = image.getexif() + orientation = exif.get(0x0112) + method = { + 2: Image.FLIP_LEFT_RIGHT, + 3: Image.ROTATE_180, + 4: Image.FLIP_TOP_BOTTOM, + 5: Image.TRANSPOSE, + 6: Image.ROTATE_270, + 7: Image.TRANSVERSE, + 8: Image.ROTATE_90 + }.get(orientation) + if method is not None: + transposed_image = image.transpose(method) + del exif[0x0112] + transposed_image.info["exif"] = exif.tobytes() + return transposed_image return image.copy() diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 7e91f5f3f..3caedbd92 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -472,7 +472,7 @@ class JpegImageFile(ImageFile.ImageFile): def _fixup_dict(src_dict): # Helper function for _getexif() # returns a dict with any single item tuples/lists as individual values - exif = ImageFile.Exif() + exif = Image.Exif() return exif._fixup_dict(src_dict) @@ -725,7 +725,7 @@ def _save(im, fp, filename): optimize = info.get("optimize", False) exif = info.get("exif", b"") - if isinstance(exif, ImageFile.Exif): + if isinstance(exif, Image.Exif): exif = exif.tobytes() # get keyword arguments diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 588cee02f..4e192ecd6 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -886,7 +886,7 @@ def _save(im, fp, filename, chunk=putchunk): exif = im.encoderinfo.get("exif", im.info.get("exif")) if exif: - if isinstance(exif, ImageFile.Exif): + if isinstance(exif, Image.Exif): exif = exif.tobytes(8) if exif.startswith(b"Exif\x00\x00"): exif = exif[6:] diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 4a11a18c1..f2a99bb9d 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -217,7 +217,7 @@ def _save_all(im, fp, filename): method = im.encoderinfo.get("method", 0) icc_profile = im.encoderinfo.get("icc_profile", "") exif = im.encoderinfo.get("exif", "") - if isinstance(exif, ImageFile.Exif): + if isinstance(exif, Image.Exif): exif = exif.tobytes() xmp = im.encoderinfo.get("xmp", "") if allow_mixed: @@ -318,7 +318,7 @@ def _save(im, fp, filename): quality = im.encoderinfo.get("quality", 80) icc_profile = im.encoderinfo.get("icc_profile", "") exif = im.encoderinfo.get("exif", "") - if isinstance(exif, ImageFile.Exif): + if isinstance(exif, Image.Exif): exif = exif.tobytes() xmp = im.encoderinfo.get("xmp", "")