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
Stop flattening EXIF IFD into getexif() #4947
Changes from all commits
faf8fad
4b14f01
b25bc40
e763f8f
c52b45d
68719fe
36a4b05
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3307,11 +3307,11 @@ def _fixup_dict(self, src_dict): | |
# returns a dict with any single item tuples/lists as individual values | ||
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: | ||
# an offset pointer to the location of the nested embedded IFD. | ||
# It should be a long, but may be corrupted. | ||
self.fp.seek(self[tag]) | ||
self.fp.seek(offset) | ||
except (KeyError, TypeError): | ||
pass | ||
else: | ||
|
@@ -3349,11 +3349,20 @@ def load(self, data): | |
self.fp.seek(self._info.next) | ||
self._info.load(self.fp) | ||
|
||
def _get_merged_dict(self): | ||
merged_dict = dict(self) | ||
|
||
# get EXIF extension | ||
ifd = self._get_ifd_dict(0x8769) | ||
if ifd: | ||
self._data.update(ifd) | ||
self._ifds[0x8769] = ifd | ||
if 0x8769 in self: | ||
ifd = self._get_ifd_dict(self[0x8769]) | ||
if 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): | ||
from . import TiffImagePlugin | ||
|
@@ -3364,91 +3373,108 @@ def tobytes(self, offset=8): | |
head = b"MM\x00\x2A\x00\x00\x00\x08" | ||
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) | ||
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 | ||
return b"Exif\x00\x00" + head + ifd.tobytes(offset) | ||
|
||
def get_ifd(self, tag): | ||
if tag not in self._ifds and tag in self: | ||
if tag in [0x8825, 0xA005]: | ||
# gpsinfo, interop | ||
self._ifds[tag] = self._get_ifd_dict(tag) | ||
elif tag == 0x927C: # makernote | ||
from .TiffImagePlugin import ImageFileDirectory_v2 | ||
|
||
if self[0x927C][:8] == b"FUJIFILM": | ||
exif_data = self[0x927C] | ||
ifd_offset = i32le(exif_data, 8) | ||
ifd_data = exif_data[ifd_offset:] | ||
|
||
makernote = {} | ||
for i in range(0, struct.unpack("<H", ifd_data[:2])[0]): | ||
ifd_tag, typ, count, data = struct.unpack( | ||
"<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}" | ||
if tag not in self._ifds: | ||
if tag in [0x8769, 0x8825]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While I can see that this would be more useful for most cases, if you wanted to actually read the IFD offset, you wouldn't be able to anymore (apart from looking directly into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see your point. |
||
# exif, 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 | ||
from .TiffImagePlugin import ImageFileDirectory_v2 | ||
|
||
if tag_data[:8] == b"FUJIFILM": | ||
ifd_offset = i32le(tag_data, 8) | ||
ifd_data = tag_data[ifd_offset:] | ||
|
||
makernote = {} | ||
for i in range(0, struct.unpack("<H", ifd_data[:2])[0]): | ||
ifd_tag, typ, count, data = struct.unpack( | ||
"<HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2] | ||
) | ||
continue | ||
|
||
if not data: | ||
continue | ||
|
||
makernote[ifd_tag] = handler( | ||
ImageFileDirectory_v2(), data, False | ||
) | ||
self._ifds[0x927C] = dict(self._fixup_dict(makernote)) | ||
elif self.get(0x010F) == "Nintendo": | ||
ifd_data = self[0x927C] | ||
|
||
makernote = {} | ||
for i in range(0, 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 = ImageFileDirectory_v2._load_dispatch[ | ||
TiffTags.FLOAT | ||
][1] | ||
camerainfo["Parallax"] = handler( | ||
ImageFileDirectory_v2(), parallax, False | ||
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 | ||
|
||
if not data: | ||
continue | ||
|
||
makernote[ifd_tag] = handler( | ||
ImageFileDirectory_v2(), data, False | ||
) | ||
|
||
self.fp.read(4) | ||
camerainfo["Category"] = self.fp.read(2) | ||
|
||
makernote = {0x1101: dict(self._fixup_dict(camerainfo))} | ||
self._ifds[0x927C] = makernote | ||
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) | ||
|
||
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.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, {}) | ||
|
||
def __str__(self): | ||
|
@@ -3468,8 +3494,6 @@ def __len__(self): | |
def __getitem__(self, tag): | ||
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]) | ||
if tag == 0x8825: | ||
self._data[tag] = self.get_ifd(tag) | ||
del self._info[tag] | ||
return self._data[tag] | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not work for interop since it's inside of Exif IFD.
I suspect same for makernote too, but since makernote is vendor specific, I think it would be safer to make it read only property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I've added another commit to cover Interop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've being thinking through this on and off for some time.
Thinking of consumer of this class, it would make sense when reading Exif IFD to get Interop value decoded into a dict at that time. Or shimmed with
ImageFileDirectory_v2
instance. Otherwise caller must know Interop tag ID, must know to callget_ifd
, etc. So that's a bit inconvenient and adds extra knowledge to consumer.Same for Makernote, however saving it back we don't want to encode it back to vendor-specific format and probably worth just copying whatever bytes were there originally.
On the other hand Exif is quite complicated and some knowledge from consumer is expected anyway.
🤷
What do you think of this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another benefit of using
ImageFileDirectory_v2
as a shim for sub-IFDs is that we get lazy loading.