Files
business-card-scanner/app/imaging.py
Lucas Orth 811dba4e98
Some checks failed
Build & Deploy / deploy (push) Failing after 11m38s
Toten Code entfernen
Ungenutzte Hilfsfunktion, ungenutzter Konfigurationswert und drei
CSS-Regeln ohne Verwendung im Markup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 11:46:52 +02:00

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