mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-26 18:06:18 +03:00
Merge pull request #4947 from radarhere/exif
This commit is contained in:
commit
5a209081b2
|
@ -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
|
||||||
|
|
|
@ -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],
|
||||||
|
|
184
src/PIL/Image.py
184
src/PIL/Image.py
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user