Merge pull request #4947 from radarhere/exif

This commit is contained in:
Hugo van Kemenade 2021-03-19 21:26:33 +02:00 committed by GitHub
commit 5a209081b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 136 additions and 98 deletions

View File

@ -264,11 +264,11 @@ class TestFileJpeg:
assert exif[0x0112] == Image.TRANSVERSE assert exif[0x0112] == Image.TRANSVERSE
# Assert that the GPS IFD is present and empty # Assert that the GPS IFD is present and empty
assert exif[0x8825] == {} assert exif.get_ifd(0x8825) == {}
transposed = ImageOps.exif_transpose(im) transposed = ImageOps.exif_transpose(im)
exif = transposed.getexif() exif = transposed.getexif()
assert exif[0x8825] == {} assert exif.get_ifd(0x8825) == {}
# Assert that it was transposed # Assert that it was transposed
assert 0x0112 not in exif assert 0x0112 not in exif

View File

@ -667,43 +667,43 @@ class TestImage:
exif = im.getexif() exif = im.getexif()
assert 258 not in exif assert 258 not in exif
assert 274 in exif assert 274 in exif
assert 40960 in exif assert 282 in exif
assert exif[40963] == 450 assert exif[296] == 2
assert exif[11] == "gThumb 3.0.1" assert exif[11] == "gThumb 3.0.1"
out = str(tmp_path / "temp.jpg") out = str(tmp_path / "temp.jpg")
exif[258] = 8 exif[258] = 8
del exif[274] del exif[274]
del exif[40960] del exif[282]
exif[40963] = 455 exif[296] = 455
exif[11] = "Pillow test" exif[11] = "Pillow test"
im.save(out, exif=exif) im.save(out, exif=exif)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
reloaded_exif = reloaded.getexif() reloaded_exif = reloaded.getexif()
assert reloaded_exif[258] == 8 assert reloaded_exif[258] == 8
assert 274 not in reloaded_exif assert 274 not in reloaded_exif
assert 40960 not in reloaded_exif assert 282 not in reloaded_exif
assert reloaded_exif[40963] == 455 assert reloaded_exif[296] == 455
assert reloaded_exif[11] == "Pillow test" assert reloaded_exif[11] == "Pillow test"
with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: # Big endian with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: # Big endian
exif = im.getexif() exif = im.getexif()
assert 258 not in exif assert 258 not in exif
assert 40962 in exif assert 306 in exif
assert exif[40963] == 200 assert exif[274] == 1
assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)" assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)"
out = str(tmp_path / "temp.jpg") out = str(tmp_path / "temp.jpg")
exif[258] = 8 exif[258] = 8
del exif[34665] del exif[306]
exif[40963] = 455 exif[274] = 455
exif[305] = "Pillow test" exif[305] = "Pillow test"
im.save(out, exif=exif) im.save(out, exif=exif)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
reloaded_exif = reloaded.getexif() reloaded_exif = reloaded.getexif()
assert reloaded_exif[258] == 8 assert reloaded_exif[258] == 8
assert 34665 not in reloaded_exif assert 306 not in reloaded_exif
assert reloaded_exif[40963] == 455 assert reloaded_exif[274] == 455
assert reloaded_exif[305] == "Pillow test" assert reloaded_exif[305] == "Pillow test"
@skip_unless_feature("webp") @skip_unless_feature("webp")
@ -756,6 +756,19 @@ class TestImage:
4098: 1704, 4098: 1704,
} }
reloaded_exif = Image.Exif()
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005)
def test_exif_ifd(self):
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
del exif.get_ifd(0x8769)[0xA005]
reloaded_exif = Image.Exif()
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_module", "test_module",
[PIL, Image], [PIL, Image],

View File

@ -3307,11 +3307,11 @@ class Exif(MutableMapping):
# returns a dict with any single item tuples/lists as individual values # returns a dict with any single item tuples/lists as individual values
return {k: self._fixup(v) for k, v in src_dict.items()} return {k: self._fixup(v) for k, v in src_dict.items()}
def _get_ifd_dict(self, tag): def _get_ifd_dict(self, offset):
try: try:
# an offset pointer to the location of the nested embedded IFD. # an offset pointer to the location of the nested embedded IFD.
# It should be a long, but may be corrupted. # It should be a long, but may be corrupted.
self.fp.seek(self[tag]) self.fp.seek(offset)
except (KeyError, TypeError): except (KeyError, TypeError):
pass pass
else: else:
@ -3349,11 +3349,20 @@ class Exif(MutableMapping):
self.fp.seek(self._info.next) self.fp.seek(self._info.next)
self._info.load(self.fp) self._info.load(self.fp)
def _get_merged_dict(self):
merged_dict = dict(self)
# get EXIF extension # get EXIF extension
ifd = self._get_ifd_dict(0x8769) if 0x8769 in self:
if ifd: ifd = self._get_ifd_dict(self[0x8769])
self._data.update(ifd) if ifd:
self._ifds[0x8769] = ifd merged_dict.update(ifd)
# GPS
if 0x8825 in self:
merged_dict[0x8825] = self._get_ifd_dict(self[0x8825])
return merged_dict
def tobytes(self, offset=8): def tobytes(self, offset=8):
from . import TiffImagePlugin from . import TiffImagePlugin
@ -3364,91 +3373,108 @@ class Exif(MutableMapping):
head = b"MM\x00\x2A\x00\x00\x00\x08" head = b"MM\x00\x2A\x00\x00\x00\x08"
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
for tag, value in self.items(): for tag, value in self.items():
if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict):
value = self.get_ifd(tag)
if (
tag == 0x8769
and 0xA005 in value
and not isinstance(value[0xA005], dict)
):
value = value.copy()
value[0xA005] = self.get_ifd(0xA005)
ifd[tag] = value ifd[tag] = value
return b"Exif\x00\x00" + head + ifd.tobytes(offset) return b"Exif\x00\x00" + head + ifd.tobytes(offset)
def get_ifd(self, tag): def get_ifd(self, tag):
if tag not in self._ifds and tag in self: if tag not in self._ifds:
if tag in [0x8825, 0xA005]: if tag in [0x8769, 0x8825]:
# gpsinfo, interop # exif, gpsinfo
self._ifds[tag] = self._get_ifd_dict(tag) if tag in self:
elif tag == 0x927C: # makernote self._ifds[tag] = self._get_ifd_dict(self[tag])
from .TiffImagePlugin import ImageFileDirectory_v2 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
from .TiffImagePlugin import ImageFileDirectory_v2
if self[0x927C][:8] == b"FUJIFILM": if tag_data[:8] == b"FUJIFILM":
exif_data = self[0x927C] ifd_offset = i32le(tag_data, 8)
ifd_offset = i32le(exif_data, 8) ifd_data = tag_data[ifd_offset:]
ifd_data = exif_data[ifd_offset:]
makernote = {} makernote = {}
for i in range(0, struct.unpack("<H", ifd_data[:2])[0]): for i in range(0, struct.unpack("<H", ifd_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack( ifd_tag, typ, count, data = struct.unpack(
"<HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2] "<HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2]
)
try:
unit_size, handler = 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. "
f"Expecting to read {size} bytes but only got "
f"{len(data)}. Skipping tag {ifd_tag}"
) )
continue try:
(
unit_size,
handler,
) = 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 not data: if len(data) != size:
continue warnings.warn(
"Possibly corrupt EXIF MakerNote data. "
f"Expecting to read {size} bytes but only got "
f"{len(data)}. Skipping tag {ifd_tag}"
)
continue
makernote[ifd_tag] = handler( if not data:
ImageFileDirectory_v2(), data, False continue
)
self._ifds[0x927C] = dict(self._fixup_dict(makernote))
elif self.get(0x010F) == "Nintendo":
ifd_data = self[0x927C]
makernote = {} makernote[ifd_tag] = handler(
for i in range(0, struct.unpack(">H", ifd_data[:2])[0]): ImageFileDirectory_v2(), data, False
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 = ImageFileDirectory_v2._load_dispatch[
TiffTags.FLOAT
][1]
camerainfo["Parallax"] = handler(
ImageFileDirectory_v2(), parallax, False
) )
self._ifds[tag] = dict(self._fixup_dict(makernote))
elif self.get(0x010F) == "Nintendo":
makernote = {}
for i in range(0, struct.unpack(">H", tag_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2]
)
if ifd_tag == 0x1101:
# CameraInfo
(offset,) = struct.unpack(">L", data)
self.fp.seek(offset)
self.fp.read(4) camerainfo = {"ModelID": self.fp.read(4)}
camerainfo["Category"] = self.fp.read(2)
makernote = {0x1101: dict(self._fixup_dict(camerainfo))} self.fp.read(4)
self._ifds[0x927C] = makernote # 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 = ImageFileDirectory_v2._load_dispatch[
TiffTags.FLOAT
][1]
camerainfo["Parallax"] = handler(
ImageFileDirectory_v2(), parallax, False
)
self.fp.read(4)
camerainfo["Category"] = self.fp.read(2)
makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
self._ifds[tag] = makernote
else:
# interop
self._ifds[tag] = self._get_ifd_dict(tag_data)
return self._ifds.get(tag, {}) return self._ifds.get(tag, {})
def __str__(self): def __str__(self):
@ -3468,8 +3494,6 @@ class Exif(MutableMapping):
def __getitem__(self, tag): def __getitem__(self, tag):
if self._info is not None and tag not in self._data and tag in self._info: if self._info is not None and tag not in self._data and tag in self._info:
self._data[tag] = self._fixup(self._info[tag]) self._data[tag] = self._fixup(self._info[tag])
if tag == 0x8825:
self._data[tag] = self.get_ifd(tag)
del self._info[tag] del self._info[tag]
return self._data[tag] return self._data[tag]

View File

@ -478,7 +478,7 @@ class JpegImageFile(ImageFile.ImageFile):
def _getexif(self): def _getexif(self):
if "exif" not in self.info: if "exif" not in self.info:
return None return None
return dict(self.getexif()) return self.getexif()._get_merged_dict()
def _getmp(self): def _getmp(self):

View File

@ -84,7 +84,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"] mptype = self.mpinfo[0xB002][frame]["Attribute"]["MPType"]
if mptype.startswith("Large Thumbnail"): if mptype.startswith("Large Thumbnail"):
exif = self.getexif() exif = self.getexif().get_ifd(0x8769)
if 40962 in exif and 40963 in exif: if 40962 in exif and 40963 in exif:
self._size = (exif[40962], exif[40963]) self._size = (exif[40962], exif[40963])
elif "exif" in self.info: elif "exif" in self.info:

View File

@ -968,7 +968,7 @@ class PngImageFile(ImageFile.ImageFile):
self.load() self.load()
if "exif" not in self.info and "Raw profile type exif" not in self.info: if "exif" not in self.info and "Raw profile type exif" not in self.info:
return None return None
return dict(self.getexif()) return self.getexif()._get_merged_dict()
def getexif(self): def getexif(self):
if "exif" not in self.info: if "exif" not in self.info:

View File

@ -184,6 +184,7 @@ TAGS_V2 = {
34665: ("ExifIFD", LONG, 1), 34665: ("ExifIFD", LONG, 1),
34675: ("ICCProfile", UNDEFINED, 1), 34675: ("ICCProfile", UNDEFINED, 1),
34853: ("GPSInfoIFD", LONG, 1), 34853: ("GPSInfoIFD", LONG, 1),
40965: ("InteroperabilityIFD", LONG, 1),
# MPInfo # MPInfo
45056: ("MPFVersion", UNDEFINED, 1), 45056: ("MPFVersion", UNDEFINED, 1),
45057: ("NumberOfImages", LONG, 1), 45057: ("NumberOfImages", LONG, 1),

View File

@ -96,7 +96,7 @@ class WebPImageFile(ImageFile.ImageFile):
def _getexif(self): def _getexif(self):
if "exif" not in self.info: if "exif" not in self.info:
return None return None
return dict(self.getexif()) return self.getexif()._get_merged_dict()
def seek(self, frame): def seek(self, frame):
if not self._seek_check(frame): if not self._seek_check(frame):