141 lines
4.6 KiB
Python
141 lines
4.6 KiB
Python
"""
|
|
Read Exif metadata from image files
|
|
Supported formats: TIFF, JPEG, PNG, Webp, HEIC
|
|
"""
|
|
|
|
from typing import Any, BinaryIO, Dict
|
|
|
|
from exifread.core.exceptions import ExifNotFound, InvalidExif
|
|
from exifread.core.exif_header import ExifHeader
|
|
from exifread.core.find_exif import determine_type, get_endian_str
|
|
from exifread.core.xmp import find_xmp_data
|
|
from exifread.exif_log import get_logger
|
|
from exifread.serialize import convert_types
|
|
from exifread.tags import DEFAULT_STOP_TAG
|
|
|
|
__version__ = "3.5.1"
|
|
|
|
logger = get_logger()
|
|
|
|
|
|
def _extract_xmp_data(hdr: ExifHeader, fh: BinaryIO):
|
|
# Easy we already have them
|
|
xmp_tag = hdr.tags.get("Image ApplicationNotes")
|
|
if xmp_tag:
|
|
logger.debug("XMP present in Exif")
|
|
xmp_bytes = bytes(xmp_tag.values)
|
|
# We need to look in the entire file for the XML
|
|
else:
|
|
xmp_bytes = find_xmp_data(fh)
|
|
if xmp_bytes:
|
|
hdr.parse_xmp(xmp_bytes)
|
|
|
|
|
|
def process_file(
|
|
fh: BinaryIO,
|
|
stop_tag: str = DEFAULT_STOP_TAG,
|
|
details=True,
|
|
strict=False,
|
|
debug=False,
|
|
truncate_tags=True,
|
|
auto_seek=True,
|
|
extract_thumbnail=True,
|
|
builtin_types=False,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Process an image file to extract EXIF metadata.
|
|
|
|
This is the function that has to deal with all the arbitrary nasty bits
|
|
of the EXIF standard.
|
|
|
|
:param fh: the file to process, must be opened in binary mode.
|
|
:param stop_tag: Stop processing when the given tag is retrieved.
|
|
:param details: If `True`, process MakerNotes.
|
|
:param strict: If `True`, raise exceptions on errors.
|
|
:param debug: Output debug information.
|
|
:param truncate_tags: If `True`, truncate the `printable` tag output.
|
|
There is no effect on tag `values`.
|
|
:param auto_seek: If `True`, automatically `seek` to the start of the file.
|
|
:param extract_thumbnail: If `True`, extract the JPEG thumbnail.
|
|
The thumbnail is not always present in the EXIF metadata.
|
|
:param builtin_types: If `True`, convert tags to standard Python types.
|
|
|
|
:returns: A `dict` containing the EXIF metadata.
|
|
The keys are a string in the format `"IFD_NAME TAG_NAME"`.
|
|
If `builtin_types` is `False`, the value will be a `IfdTag` class, or bytes.
|
|
IF `builtin_types` is `True`, the value will be a standard Python type.
|
|
"""
|
|
|
|
if auto_seek:
|
|
fh.seek(0)
|
|
|
|
try:
|
|
offset, endian_bytes, fake_exif = determine_type(fh)
|
|
except ExifNotFound as err:
|
|
logger.warning(err)
|
|
return {}
|
|
except InvalidExif as err:
|
|
logger.debug(err)
|
|
return {}
|
|
|
|
endian_str, endian_type = get_endian_str(endian_bytes)
|
|
# deal with the EXIF info we found
|
|
logger.debug("Endian format is %s (%s)", endian_str, endian_type)
|
|
|
|
hdr = ExifHeader(
|
|
fh, endian_str, offset, fake_exif, strict, debug, details, truncate_tags
|
|
)
|
|
thumb_ifd = 0
|
|
ctr = 0
|
|
for ifd in hdr.list_ifd():
|
|
if ctr == 0:
|
|
ifd_name = "Image"
|
|
elif ctr == 1:
|
|
ifd_name = "Thumbnail"
|
|
thumb_ifd = ifd
|
|
else:
|
|
ifd_name = "IFD %d" % ctr
|
|
logger.debug("IFD %d (%s) at offset %s:", ctr, ifd_name, ifd)
|
|
hdr.dump_ifd(ifd=ifd, ifd_name=ifd_name, stop_tag=stop_tag)
|
|
ctr += 1
|
|
# EXIF IFD
|
|
exif_off = hdr.tags.get("Image ExifOffset")
|
|
if exif_off:
|
|
logger.debug("Exif SubIFD at offset %s:", exif_off.values[0])
|
|
hdr.dump_ifd(ifd=exif_off.values[0], ifd_name="EXIF", stop_tag=stop_tag)
|
|
|
|
# EXIF SubIFD
|
|
sub_ifds = hdr.tags.get("Image SubIFDs")
|
|
if details and sub_ifds:
|
|
for subifd_id, subifd_offset in enumerate(sub_ifds.values):
|
|
logger.debug("Exif SubIFD%d at offset %d:", subifd_id, subifd_offset)
|
|
hdr.dump_ifd(
|
|
ifd=subifd_offset, ifd_name=f"EXIF SubIFD{subifd_id}", stop_tag=stop_tag
|
|
)
|
|
|
|
# deal with MakerNote contained in EXIF IFD
|
|
# (Some apps use MakerNote tags but do not use a format for which we
|
|
# have a description, do not process these).
|
|
if details and "EXIF MakerNote" in hdr.tags and "Image Make" in hdr.tags:
|
|
try:
|
|
hdr.decode_maker_note()
|
|
except ValueError as err:
|
|
if not strict:
|
|
logger.debug("Failed to decode EXIF MakerNote: %s", str(err))
|
|
else:
|
|
raise err
|
|
|
|
# extract thumbnails
|
|
if thumb_ifd and extract_thumbnail:
|
|
hdr.extract_tiff_thumbnail(thumb_ifd)
|
|
hdr.extract_jpeg_thumbnail()
|
|
|
|
# parse XMP tags (experimental)
|
|
if debug and details:
|
|
_extract_xmp_data(hdr=hdr, fh=fh)
|
|
|
|
if builtin_types:
|
|
return convert_types(hdr.tags)
|
|
|
|
return hdr.tags
|