"""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()