Files

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