64 lines
1.8 KiB
Python
64 lines
1.8 KiB
Python
"""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<Y>\d{4})[:\-]?(?P<M>\d{2})[:\-]?(?P<D>\d{2})"
|
|
r"(?:[ T_])?"
|
|
r"(?P<h>\d{2}):?(?P<m>\d{2}):?(?P<s>\d{2})"
|
|
r"(?:[.,](?P<sub>\d+))?"
|
|
r"(?:\s*(?P<tz>Z|[+-]\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)
|