Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added IFD enum to ExifTags #6748

Merged
merged 5 commits into from Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -415,6 +415,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
thumbnailOffset = ifd.get(513)
if thumbnailOffset is not None:
try:
thumbnailOffset += self._exif_offset
except AttributeError:
pass
self.fp.seek(thumbnailOffset)
data = self.fp.read(ifd.get(514))
fp = io.BytesIO(data)

with open(fp) as im:
if thumbnailOffset is None:
hugovk marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -89,6 +89,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