Eigene Bilder importieren und auf Slides legen

Bilder aus dem Finder kommen jetzt ueber die Dateiauswahl oder per Ziehen
direkt auf eine Slide herein. Beim Ablegen wird das Bild dort zum zweiten
Bild und laesst sich im Bearbeiten-Modus frei schieben und in der Groesse
aendern; ueber den Knopf importiert man nur und ordnet danach unter Inhalt
zu. Das zweite Bild laesst sich dort auch wieder abwaehlen.

Die Datei geht byteweise unveraendert an den neuen Endpunkt /upload und
liegt danach unter assets/eigene/. Nichts wird neu kodiert, skaliert oder
umbenannt -- nur Zeichen, die in einem Dateinamen nichts zu suchen haben,
weichen einem Bindestrich. Der Umweg ueber den Server ist Absicht: ein Foto
als Base64 im Projektstand waere groesser als der gesamte uebrige Stand und
machte jeden Diff unlesbar. So steht dort nur der Pfad, und das Bild liegt
mit im Repo wie jedes andere Asset auch.

Die Auswahllisten holt der Generator beim Start ueber /bilder vom Server
statt sie im Code zu pflegen. Importierte stehen deshalb sofort darin, mit
einem Stern gekennzeichnet. Beim Zuweisen folgt das Rahmenformat dem
Seitenverhaeltnis der Aufnahme.

Geprueft ueber das DevTools-Protokoll: ablegen, Bytes gegen die Quelle
vergleichen, Format, Verschieben im Bearbeiten-Modus, Export und Neustart.
Die abgelegte Datei war byteweise identisch mit der Quelle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-07 18:09:40 +02:00
1 parent 1cce5ec492
commit 8e1ac0d2a9
3 files changed
+281 -7

No files matched your search

+71
View File
@@ -11,6 +11,12 @@ Der Projektstand liegt nicht mehr nur im localStorage des Browsers, sondern
unter state/<projekt>.json im Repo. Nur so ueberlebt er einen Rechnerwechsel:
gepusht und gezogen hat die andere Maschine denselben Stand. Der localStorage
bleibt als Zwischenspeicher, die Datei ist die Wahrheit.
Eigene Bilder kommen ueber /upload herein und landen unverfaelscht als Datei
unter assets/eigene/. Der Weg ueber den Projektstand waere der falsche: ein
2-MB-PNG als Base64 blaeht state/post1.json auf und macht jeden Diff
unlesbar. So steht im Stand nur der Pfad, und das Bild liegt mit im Repo --
dasselbe Prinzip wie bei allen anderen Assets.
"""
import http.server
@@ -25,9 +31,15 @@ PORT = 4321
ROOT = Path(__file__).resolve().parent
TARGET = Path.home() / "Downloads"
STATE_DIR = ROOT / "state"
EIGENE_DIR = ROOT / "assets" / "eigene"
SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]{1,120}$")
SAFE_PROJECT = re.compile(r"^[a-z0-9][a-z0-9-]{0,39}$")
MAX_STATE = 4 * 1024 * 1024
MAX_BILD = 40 * 1024 * 1024
# Nur verlustfreie oder unveraenderte Rasterbilder. Die Bytes werden nicht
# angefasst, deshalb bleibt ein PNG genau das PNG, das der Finder geliefert hat.
BILD_ENDUNGEN = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif"}
BILD_ORDNER = ("assets/screens", "assets/eigene")
class Handler(http.server.SimpleHTTPRequestHandler):
@@ -90,11 +102,70 @@ class Handler(http.server.SimpleHTTPRequestHandler):
tmp.replace(p)
self._json(200, '{"ok":true}')
def do_GET(self):
"""Vor der Dateiauslieferung nur die Bilderliste abfangen."""
if urllib.parse.urlparse(self.path).path == "/bilder":
self._liste_bilder()
return
super().do_GET()
def _liste_bilder(self):
"""Alle verwendbaren Bilder, damit die Auswahl im Generator nicht im
Code gepflegt werden muss und importierte sofort auftauchen."""
raus = []
for ordner in BILD_ORDNER:
verz = ROOT / ordner
if not verz.is_dir():
continue
for f in sorted(verz.iterdir()):
if f.is_file() and f.suffix.lower() in BILD_ENDUNGEN:
raus.append({"pfad": "./" + ordner + "/" + f.name,
"name": f.stem, "ordner": ordner.split("/")[-1]})
self._json(200, json.dumps(raus, ensure_ascii=False))
def _upload(self):
"""Ein eigenes Bild aufnehmen. Die Bytes gehen unveraendert auf die
Platte: nicht neu kodiert, nicht skaliert, nicht umbenannt ausser um
gefaehrliche Zeichen. Verlustfrei heisst hier woertlich unangetastet."""
name = os.path.basename(urllib.parse.unquote(self.headers.get("X-Filename", "")))
stamm, endung = os.path.splitext(name)
endung = endung.lower()
if endung not in BILD_ENDUNGEN:
self._json(400, '{"error":"kein Bildformat"}')
return
# Aus dem Stamm wird ein unverfaenglicher Dateiname, der Rest faellt weg.
stamm = re.sub(r"[^A-Za-z0-9._-]+", "-", stamm).strip("-.") or "bild"
stamm = stamm[:80]
length = int(self.headers.get("Content-Length", 0))
if length <= 0 or length > MAX_BILD:
self._json(400, '{"error":"ungueltige Groesse"}')
return
daten = self.rfile.read(length)
EIGENE_DIR.mkdir(parents=True, exist_ok=True)
ziel = (EIGENE_DIR / (stamm + endung)).resolve()
if ziel.parent != EIGENE_DIR.resolve():
self._json(400, '{"error":"ungueltiger Pfad"}')
return
# Gleicher Inhalt, gleicher Name: nicht doppelt ablegen.
n = 1
while ziel.exists() and ziel.read_bytes() != daten:
ziel = EIGENE_DIR / (stamm + "-" + str(n) + endung)
n += 1
ziel.write_bytes(daten)
pfad = "./assets/eigene/" + ziel.name
print(f"importiert: {ziel.name} ({len(daten)/1024:.0f} KB)")
self._json(200, json.dumps({"pfad": pfad, "name": ziel.stem}))
def do_POST(self):
path = urllib.parse.urlparse(self.path).path
if path == "/state":
self._put_state()
return
if path == "/upload":
self._upload()
return
if path != "/save":
self.send_error(404)
return