Skip to content

Commit

Permalink
Merge pull request #6748 from radarhere/exif_ifd
Browse files Browse the repository at this point in the history
Added IFD enum to ExifTags
  • Loading branch information
radarhere committed Dec 13, 2022
2 parents 56964da + 5301b86 commit 5257d56
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 56 deletions.
Binary file added Tests/images/flower_thumbnail.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions Tests/test_file_jpeg.py
Expand Up @@ -442,6 +442,13 @@ def test_exif(self):
info = im._getexif()
assert info[305] == "Adobe Photoshop CS Macintosh"

def test_get_child_images(self):
with Image.open("Tests/images/flower.jpg") as im:
ims = im.get_child_images()

assert len(ims) == 1
assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png")

def test_mp(self):
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert im._getmp() is None
Expand Down
21 changes: 20 additions & 1 deletion Tests/test_image.py
Expand Up @@ -7,7 +7,14 @@

import pytest

from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError, features
from PIL import (
ExifTags,
Image,
ImageDraw,
ImagePalette,
UnidentifiedImageError,
features,
)

from .helper import (
assert_image_equal,
Expand Down Expand Up @@ -808,6 +815,18 @@ def test_exif_interop(self):
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005)

def test_exif_ifd1(self):
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
assert exif.get_ifd(ExifTags.IFD.IFD1) == {
513: 2036,
514: 5448,
259: 6,
296: 2,
282: 180.0,
283: 180.0,
}

def test_exif_ifd(self):
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
Expand Down
7 changes: 7 additions & 0 deletions docs/reference/ExifTags.rst
Expand Up @@ -31,6 +31,13 @@ which provide constants and clear-text names for various well-known EXIF tags.
>>> Interop(4096).name
'RelatedImageFileFormat'

.. py:data:: IFD
>>> from PIL.ExifTags import IFD
>>> IFD.Exif.value
34665
>>> IFD(34665).name
'Exif'

Two of these values are also exposed as dictionaries.

Expand Down
8 changes: 8 additions & 0 deletions src/PIL/ExifTags.py
Expand Up @@ -346,3 +346,11 @@ class Interop(IntEnum):
RelatedImageFileFormat = 4096
RelatedImageWidth = 4097
RleatedImageHeight = 4098


class IFD(IntEnum):
Exif = 34665
GPSInfo = 34853
Makernote = 37500
Interop = 40965
IFD1 = -1
95 changes: 75 additions & 20 deletions src/PIL/Image.py
Expand Up @@ -47,7 +47,14 @@
# VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0.
# Use __version__ instead.
from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
from . import (
ExifTags,
ImageMode,
TiffTags,
UnidentifiedImageError,
__version__,
_plugins,
)
from ._binary import i32le, o32be, o32le
from ._deprecate import deprecate
from ._util import DeferredError, is_path
Expand Down Expand Up @@ -1447,6 +1454,49 @@ def _reload_exif(self):
self._exif._loaded = False
self.getexif()

def get_child_images(self):
child_images = []
exif = self.getexif()
ifds = []
if ExifTags.Base.SubIFDs in exif:
subifd_offsets = exif[ExifTags.Base.SubIFDs]
if subifd_offsets:
if not isinstance(subifd_offsets, tuple):
subifd_offsets = (subifd_offsets,)
for subifd_offset in subifd_offsets:
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
if ifd1 and ifd1.get(513):
ifds.append((ifd1, exif._info.next))

offset = None
for ifd, ifd_offset in ifds:
current_offset = self.fp.tell()
if offset is None:
offset = current_offset

fp = self.fp
thumbnail_offset = ifd.get(513)
if thumbnail_offset is not None:
try:
thumbnail_offset += self._exif_offset
except AttributeError:
pass
self.fp.seek(thumbnail_offset)
data = self.fp.read(ifd.get(514))
fp = io.BytesIO(data)

with open(fp) as im:
if thumbnail_offset is None:
im._frame_pos = [ifd_offset]
im._seek(0)
im.load()
child_images.append(im)

if offset is not None:
self.fp.seek(offset)
return child_images

def getim(self):
"""
Returns a capsule that points to the internal image memory.
Expand Down Expand Up @@ -3598,14 +3648,16 @@ def _get_merged_dict(self):
merged_dict = dict(self)

# get EXIF extension
if 0x8769 in self:
ifd = self._get_ifd_dict(self[0x8769])
if ExifTags.IFD.Exif in self:
ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif])
if ifd:
merged_dict.update(ifd)

# GPS
if 0x8825 in self:
merged_dict[0x8825] = self._get_ifd_dict(self[0x8825])
if ExifTags.IFD.GPSInfo in self:
merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict(
self[ExifTags.IFD.GPSInfo]
)

return merged_dict

Expand All @@ -3615,31 +3667,34 @@ def tobytes(self, offset=8):
head = self._get_head()
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
for tag, value in self.items():
if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict):
if tag in [
ExifTags.IFD.Exif,
ExifTags.IFD.GPSInfo,
] and not isinstance(value, dict):
value = self.get_ifd(tag)
if (
tag == 0x8769
and 0xA005 in value
and not isinstance(value[0xA005], dict)
tag == ExifTags.IFD.Exif
and ExifTags.IFD.Interop in value
and not isinstance(value[ExifTags.IFD.Interop], dict)
):
value = value.copy()
value[0xA005] = self.get_ifd(0xA005)
value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop)
ifd[tag] = value
return b"Exif\x00\x00" + head + ifd.tobytes(offset)

def get_ifd(self, tag):
if tag not in self._ifds:
if tag in [0x8769, 0x8825]:
# exif, gpsinfo
if tag == ExifTags.IFD.IFD1:
if self._info is not None:
self._ifds[tag] = self._get_ifd_dict(self._info.next)
elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]:
if tag in self:
self._ifds[tag] = self._get_ifd_dict(self[tag])
elif tag in [0xA005, 0x927C]:
# interop, makernote
if 0x8769 not in self._ifds:
self.get_ifd(0x8769)
tag_data = self._ifds[0x8769][tag]
if tag == 0x927C:
# makernote
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]:
if ExifTags.IFD.Exif not in self._ifds:
self.get_ifd(ExifTags.IFD.Exif)
tag_data = self._ifds[ExifTags.IFD.Exif][tag]
if tag == ExifTags.IFD.Makernote:
from .TiffImagePlugin import ImageFileDirectory_v2

if tag_data[:8] == b"FUJIFILM":
Expand Down Expand Up @@ -3715,7 +3770,7 @@ def get_ifd(self, tag):
makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
self._ifds[tag] = makernote
else:
# interop
# Interop
self._ifds[tag] = self._get_ifd_dict(tag_data)
return self._ifds.get(tag, {})

Expand Down
1 change: 1 addition & 0 deletions src/PIL/JpegImagePlugin.py
Expand Up @@ -90,6 +90,7 @@ def APP(self, marker):
if "exif" not in self.info:
# extract EXIF information (incomplete)
self.info["exif"] = s # FIXME: value will change
self._exif_offset = self.fp.tell() - n + 6
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete)
self.info["flashpix"] = s # FIXME: value will change
Expand Down
11 changes: 9 additions & 2 deletions src/PIL/MpoImagePlugin.py
Expand Up @@ -22,7 +22,14 @@
import os
import struct

from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin
from . import (
ExifTags,
Image,
ImageFile,
ImageSequence,
JpegImagePlugin,
TiffImagePlugin,
)
from ._binary import i16be as i16
from ._binary import o32le

Expand Down Expand Up @@ -137,7 +144,7 @@ def seek(self, frame):

mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"]
if mptype.startswith("Large Thumbnail"):
exif = self.getexif().get_ifd(0x8769)
exif = self.getexif().get_ifd(ExifTags.IFD.Exif)
if 40962 in exif and 40963 in exif:
self._size = (exif[40962], exif[40963])
elif "exif" in self.info:
Expand Down
33 changes: 0 additions & 33 deletions src/PIL/TiffImagePlugin.py
Expand Up @@ -1153,39 +1153,6 @@ def tell(self):
"""Return the current frame number"""
return self.__frame

def get_child_images(self):
if SUBIFD not in self.tag_v2:
return []
child_images = []
exif = self.getexif()
offset = None
for im_offset in self.tag_v2[SUBIFD]:
# reset buffered io handle in case fp
# was passed to libtiff, invalidating the buffer
current_offset = self._fp.tell()
if offset is None:
offset = current_offset

fp = self._fp
ifd = exif._get_ifd_dict(im_offset)
jpegInterchangeFormat = ifd.get(513)
if jpegInterchangeFormat is not None:
fp.seek(jpegInterchangeFormat)
jpeg_data = fp.read(ifd.get(514))

fp = io.BytesIO(jpeg_data)

with Image.open(fp) as im:
if jpegInterchangeFormat is None:
im._frame_pos = [im_offset]
im._seek(0)
im.load()
child_images.append(im)

if offset is not None:
self._fp.seek(offset)
return child_images

def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
Expand Down

0 comments on commit 5257d56

Please sign in to comment.