"""Small, shared helpers for values returned by ExifTool.""" from __future__ import annotations import math import re from collections.abc import Iterable, Mapping from datetime import datetime, timedelta, timezone DATETIME_RE = re.compile( r"(?P\d{4})[:\-]?(?P\d{2})[:\-]?(?P\d{2})" r"(?:[ T_])?" r"(?P\d{2}):?(?P\d{2}):?(?P\d{2})" r"(?:[.,](?P\d+))?" r"(?:\s*(?PZ|[+-]\d{2}:?\d{2}))?" ) def first_tag(metadata: Mapping[str, object], tags: Iterable[str]) -> object | None: for tag in tags: if tag in metadata: return metadata[tag] return None def tag_value(metadata: Mapping[str, object], tag: str) -> str | None: value = metadata.get(tag) return None if value is None else str(value) def parse_exif_datetime(value: object, assume_utc: bool = False) -> datetime | None: if value is None: return None match = DATETIME_RE.search(str(value).strip()) if not match: return None try: tz_text = match.group("tz") if tz_text == "Z": tzinfo = timezone.utc elif tz_text: sign = 1 if tz_text[0] == "+" else -1 digits = tz_text[1:].replace(":", "") tzinfo = timezone(sign * timedelta(hours=int(digits[:2]), minutes=int(digits[2:]))) else: tzinfo = None parsed = datetime( int(match.group("Y")), int(match.group("M")), int(match.group("D")), int(match.group("h")), int(match.group("m")), int(match.group("s")), tzinfo=tzinfo, ) return parsed.replace(tzinfo=timezone.utc) if assume_utc and tzinfo is None else parsed except ValueError: return None def round_half_up(value: float) -> int: return math.floor(value + 0.5)