Selbst gehostete PWA, die Visitenkarten von einem Foto freistellt, ausliest und als Kontakt bereitstellt. - Stapelscan: OpenCV findet die Kartenrechtecke über mehrere Binärmasken, entzerrt sie perspektivisch und schneidet sie einzeln aus. Ohne Fund gilt das ganze Foto als eine Karte. - Extraktion: ein Aufruf je Zuschnitt an das Vision-Modell mit JSON-Schema. Kein vorgeschaltetes OCR - das würde Layout und Schriftgrößen wegwerfen, aus denen die Feldzuordnung entsteht. - Metadaten: Aufnahmezeit und GPS aus den EXIF-Daten des Fotos, Ortsname über Nominatim, Browserstandort nur als Rückfallebene. - Übersicht mit Volltextsuche und Filtern, Detailansicht mit Korrekturmaske. - Notizfeld je Karte, Erinnerungen per Mail inklusive Nachholen verpasster Termine nach einem Neustart. - vCard 3.0 einzeln und als Sammeldatei, Karte gilt danach als exportiert. - Anmeldung über ein Passwort, Sitzung als signiertes Cookie. - Deployment per Tag-Push nach den Konventionen in DEPLOY.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
73 lines
1.8 KiB
Python
73 lines
1.8 KiB
Python
import io
|
|
|
|
from PIL import Image
|
|
|
|
from app import imaging
|
|
|
|
|
|
def _jpeg_with_exif(tags: dict) -> bytes:
|
|
image = Image.new("RGB", (64, 40), "white")
|
|
exif = Image.Exif()
|
|
for tag, value in tags.items():
|
|
exif[tag] = value
|
|
buffer = io.BytesIO()
|
|
image.save(buffer, format="JPEG", exif=exif.tobytes())
|
|
return buffer.getvalue()
|
|
|
|
|
|
def test_aufnahmezeit_wird_gelesen():
|
|
raw = _jpeg_with_exif({0x9003: "2026:09:01 14:23:05"})
|
|
|
|
meta = imaging.read_metadata(raw)
|
|
|
|
assert meta["captured_at"].startswith("2026-09-01T14:23:05")
|
|
|
|
|
|
def test_gps_wird_in_dezimalgrad_umgerechnet():
|
|
raw = _jpeg_with_exif(
|
|
{0x8825: {1: "N", 2: (52.0, 19.0, 18.0), 3: "E", 4: (9.0, 48.0, 28.0)}}
|
|
)
|
|
|
|
meta = imaging.read_metadata(raw)
|
|
|
|
assert meta["lat"] == 52.321667
|
|
assert meta["lon"] == 9.807778
|
|
|
|
|
|
def test_suedliche_und_westliche_koordinaten_werden_negativ():
|
|
raw = _jpeg_with_exif(
|
|
{0x8825: {1: "S", 2: (33.0, 55.0, 0.0), 3: "W", 4: (18.0, 25.0, 0.0)}}
|
|
)
|
|
|
|
meta = imaging.read_metadata(raw)
|
|
|
|
assert meta["lat"] < 0
|
|
assert meta["lon"] < 0
|
|
|
|
|
|
def test_foto_ohne_exif_liefert_leere_metadaten():
|
|
buffer = io.BytesIO()
|
|
Image.new("RGB", (32, 32), "white").save(buffer, format="JPEG")
|
|
|
|
meta = imaging.read_metadata(buffer.getvalue())
|
|
|
|
assert meta == {"captured_at": None, "lat": None, "lon": None}
|
|
|
|
|
|
def test_kaputte_datei_wirft_nicht():
|
|
assert imaging.read_metadata(b"kein bild") == {
|
|
"captured_at": None,
|
|
"lat": None,
|
|
"lon": None,
|
|
}
|
|
|
|
|
|
def test_dekodieren_liefert_bgr_array():
|
|
buffer = io.BytesIO()
|
|
Image.new("RGB", (20, 10), (255, 0, 0)).save(buffer, format="PNG")
|
|
|
|
array = imaging.decode(buffer.getvalue())
|
|
|
|
assert array.shape == (10, 20, 3)
|
|
assert tuple(array[0, 0]) == (0, 0, 255) # Rot liegt in BGR hinten
|