import json import logging import os from datetime import datetime import pytz from PIL import Image, IptcImagePlugin, TiffImagePlugin logger = logging.getLogger(__name__) """ Required exif values """ TAGS_EXIF = { 0x9003: "DateTimeOriginal", 0x829D: "FNumber", 0x829A: "ExposureTime", 0x8827: "ISOSpeedRatings", 0x9204: "ExposureBiasValue", 0x8822: "ExposureProgram", 0x9207: "MeteringMode", 0x920A: "FocalLength", 0xA405: "FocalLengthIn35mmFilm", 0x0110: "Model", 0xA434: "LensModel", } """ Required iptc values """ TAGS_IPTC = { (2, 25): "Keywords", (2, 55): "DateCreated", (2, 60): "TimeCreated", } class Exif: def __init__(self, filename: str): self.filename = filename self._exif = {} self._iptc = {} self.data = {} self._process_image() self._read_exif() self._read_iptc() def _process_image(self): """ Open and verify image file """ try: with Image.open(self.filename) as self.img: self.img.verify() except (OSError, IOError) as e: logger.exception('Could not open or verify image: %s', self.filename) raise e def _read_exif(self): """ Read and process EXIF metadata """ try: exif_data = self.img._getexif() if exif_data is not None: self._exif = { TAGS_EXIF[k]: v for k, v in exif_data.items() if k in TAGS_EXIF } for key, value in self._exif.items(): if isinstance(value, TiffImagePlugin.IFDRational): self.data[key] = float(value) elif isinstance(value, tuple): self.data[key] = tuple(float(t) if isinstance(t, TiffImagePlugin.IFDRational) else t for t in value) elif isinstance(value, bytes): self.data[key] = value.decode('utf-8') else: self.data[key] = value except AttributeError: logger.warning('No EXIF metadata found for: %s', self.filename) except Exception as e: logger.warning('Could not read EXIF metadata from: %s, Error: %s', self.filename, e) def _read_iptc(self): """ Read and process IPTC metadata """ try: iptc_data = IptcImagePlugin.getiptcinfo(self.img) if iptc_data is not None: self._iptc = { TAGS_IPTC[k]: v for k, v in iptc_data.items() if k in TAGS_IPTC } for key, value in self._iptc.items(): if isinstance(value, list): self.data[key] = [x.decode('utf-8') for x in value] else: self.data[key] = value.decode('utf-8') except Exception as e: logger.warning('Could not read IPTC metadata from: %s, Error: %s', self.filename, e) def datetimeoriginal(self, timezone='UTC'): """ Return DateTimeOriginal with timezone, or fallback to file creation date if not available. """ if 'DateTimeOriginal' in self.data: try: return datetime.strptime(self.data['DateTimeOriginal'], "%Y:%m:%d %H:%M:%S").astimezone(pytz.timezone(timezone)) except ValueError: logger.warning('Invalid EXIF DateTimeOriginal format in: %s', self.filename) # If EXIF DateTimeOriginal is not available, fallback to file creation date try: creation_time = os.path.getctime(self.filename) return datetime.fromtimestamp(creation_time).astimezone(pytz.timezone(timezone)) except Exception as e: logger.warning('Could not retrieve file creation date for: %s, Error: %s', self.filename, e) return datetime.now(pytz.timezone(timezone)) # Fallback to current time if all else fails def json(self): """ Return exif data in json format """ return json.dumps(self.data) def keywords(self): """ Return Keywords list """ return self.data.get('Keywords', []) def __str__(self): return self.filename