Some checks failed
Build & Deploy / deploy (push) Failing after 11m38s
Ungenutzte Hilfsfunktion, ungenutzter Konfigurationswert und drei CSS-Regeln ohne Verwendung im Markup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
82 lines
2.7 KiB
Python
82 lines
2.7 KiB
Python
"""EXIF-Auswertung und Bild-Hilfsfunktionen."""
|
|
import io
|
|
from datetime import datetime, timezone
|
|
|
|
import numpy as np
|
|
from PIL import Image, ImageOps
|
|
|
|
# EXIF-Tags, die uns interessieren (Nummern statt Namen: stabil ueber Pillow-Versionen).
|
|
_TAG_DATETIME_ORIGINAL = 0x9003
|
|
_TAG_OFFSET_ORIGINAL = 0x9011
|
|
_TAG_GPS_IFD = 0x8825
|
|
_GPS_LAT_REF, _GPS_LAT = 1, 2
|
|
_GPS_LON_REF, _GPS_LON = 3, 4
|
|
|
|
|
|
def _to_degrees(value) -> float | None:
|
|
try:
|
|
d, m, s = (float(x) for x in value)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
return d + m / 60.0 + s / 3600.0
|
|
|
|
|
|
def read_metadata(raw: bytes) -> dict:
|
|
"""Aufnahmezeitpunkt und GPS aus den EXIF-Daten des Originalfotos.
|
|
|
|
Der Zeitpunkt des Fotos ist verlaesslicher als der des Uploads: Karten
|
|
werden oft erst Stunden spaeter am Schreibtisch abfotografiert.
|
|
"""
|
|
out: dict = {"captured_at": None, "lat": None, "lon": None}
|
|
try:
|
|
exif = Image.open(io.BytesIO(raw)).getexif()
|
|
except Exception:
|
|
return out
|
|
if not exif:
|
|
return out
|
|
|
|
taken = exif.get(_TAG_DATETIME_ORIGINAL)
|
|
if not taken:
|
|
ifd = exif.get_ifd(0x8769) # Exif-IFD
|
|
taken = ifd.get(_TAG_DATETIME_ORIGINAL) if ifd else None
|
|
offset = ifd.get(_TAG_OFFSET_ORIGINAL) if ifd else None
|
|
else:
|
|
offset = exif.get(_TAG_OFFSET_ORIGINAL)
|
|
if taken:
|
|
try:
|
|
dt = datetime.strptime(str(taken).strip(), "%Y:%m:%d %H:%M:%S")
|
|
iso = f"{dt.isoformat()}{offset}" if offset else dt.isoformat()
|
|
parsed = datetime.fromisoformat(iso)
|
|
if parsed.tzinfo is None:
|
|
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
out["captured_at"] = parsed.astimezone(timezone.utc).isoformat()
|
|
except ValueError:
|
|
pass
|
|
|
|
gps = exif.get_ifd(_TAG_GPS_IFD)
|
|
if gps:
|
|
lat = _to_degrees(gps.get(_GPS_LAT))
|
|
lon = _to_degrees(gps.get(_GPS_LON))
|
|
if lat is not None and lon is not None:
|
|
if str(gps.get(_GPS_LAT_REF, "N")).upper().startswith("S"):
|
|
lat = -lat
|
|
if str(gps.get(_GPS_LON_REF, "E")).upper().startswith("W"):
|
|
lon = -lon
|
|
out["lat"], out["lon"] = round(lat, 6), round(lon, 6)
|
|
return out
|
|
|
|
|
|
def decode(raw: bytes) -> np.ndarray:
|
|
"""Bytes zu einem BGR-Array, EXIF-Drehung bereits angewandt."""
|
|
image = Image.open(io.BytesIO(raw))
|
|
image = ImageOps.exif_transpose(image).convert("RGB")
|
|
return np.asarray(image)[:, :, ::-1].copy()
|
|
|
|
|
|
def encode_jpeg(rgb_or_bgr: np.ndarray, quality: int = 88, bgr: bool = True) -> bytes:
|
|
array = rgb_or_bgr[:, :, ::-1] if bgr else rgb_or_bgr
|
|
buffer = io.BytesIO()
|
|
Image.fromarray(array).save(buffer, format="JPEG", quality=quality, optimize=True)
|
|
return buffer.getvalue()
|
|
|