From 84e82252dfe46f6d205143c19f9cc86c9346c1fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9DNyymix=E2=80=9D?= Date: Sun, 5 Jan 2025 17:23:30 +0200 Subject: [PATCH] Add exif.py --- gallery/exif.py | 122 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 gallery/exif.py diff --git a/gallery/exif.py b/gallery/exif.py new file mode 100644 index 0000000..10706bc --- /dev/null +++ b/gallery/exif.py @@ -0,0 +1,122 @@ +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