Bilder werden vor dem Hochladen aufbereitet

Das Bild "Event des Jahres" liegt als PNG mit 2524 KB auf dem Server
und macht damit zwei Drittel der gesamten Startseite aus. Angezeigt
wird es mit 246 bis 420 Punkten -- die Datei hat 1672. PNG ist fuer ein
Foto ausserdem das falsche Format.

Der Fehler passiert beim Hochladen: Ein Handy liefert Fotos in voller
Kameraaufloesung, und niemand denkt vorher ans Verkleinern. Es ist auch
nicht die Aufgabe dessen, der ein Bild aussucht -- sondern die der
Seite, die es entgegennimmt.

IM BROWSER, NICHT AUF DEM SERVER

Serverseitig braeuchte es eine Bildbibliothek mit nativem Code,
installiert in einem Verzeichnis, an das ich nicht herankomme. Der
Browser kann das ohnehin: Canvas skaliert und kodiert seit jeher.

Nebeneffekt: Schon der Upload wird kleiner. Wer vom Handy aus ein
8-MB-Foto hochlaedt, wartet sonst am Mobilfunknetz.

DREI ENTSCHEIDUNGEN

1. Kleine PNGs bleiben unangetastet. Ein Logo mit durchsichtigem
   Hintergrund wuerde als JPEG einen schwarzen Kasten bekommen. Die
   Grenze liegt bei 400 KB -- darunter ist es wahrscheinlich eine
   Grafik, darueber praktisch immer ein Foto.

2. Wird die Datei NICHT kleiner, bleibt das Original. Bei bereits gut
   komprimierten Bildern kann erneutes Kodieren sogar zulegen -- und
   Qualitaet kosten fuer nichts.

3. Schlaegt irgendetwas fehl, wird das Original hochgeladen. Ein
   misslungenes Verkleinern darf niemals einen Upload verhindern.

GEPRUEFT

Grosses PNG 3000px: 130 -> 55 KB, auf 1600px begrenzt.
Grosses JPEG 3000px: 269 -> 131 KB.
Kleines PNG: unveraendert, bleibt PNG.
Keine Skriptfehler.

Gilt fuer alle drei Upload-Stellen: Event-Bild, Team-Foto,
Stimmen-Bild.

Das vorhandene 2524-KB-PNG bleibt davon unberuehrt -- es liegt schon
auf dem Server. Ein einmaliges Neu-Hochladen ueber die Verwaltung
ersetzt es durch die aufbereitete Fassung.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-27 00:49:49 +02:00
co-authored by Claude Opus 5
parent 5deb920f39
commit b99e3043b6
+114 -3
View File
@@ -1255,6 +1255,117 @@
<script>
(function () {
const API_BASE_URL = "https://postfach.dogfather-universe.com";
/* =====================================================================
BILDER VOR DEM HOCHLADEN AUFBEREITEN
ANLASS (27.08.2026)
Das Bild "Event des Jahres" lag als PNG mit 2524 KB auf dem Server
und machte damit zwei Drittel der gesamten Startseite aus. Es wird
mit 246 bis 420 Punkten Breite angezeigt -- die Datei hat 1672.
PNG ist fuer ein Foto ausserdem das falsche Format; dieselbe
Aufnahme als JPEG waere rund ein Fuenfzehntel so gross.
Der Fehler passiert beim Hochladen: Ein Handy liefert Fotos in
voller Kameraaufloesung, und niemand denkt vorher ans Verkleinern.
Es ist auch nicht die Aufgabe dessen, der ein Bild aussucht.
WARUM IM BROWSER UND NICHT AUF DEM SERVER
Serverseitig braeuchte es eine Bildbibliothek (sharp o.ae.) --
eine neue Abhaengigkeit mit nativem Code, installiert in einem
Verzeichnis, an das ich nicht herankomme. Der Browser kann das
ohnehin: Canvas skaliert und kodiert seit jeher.
Nebeneffekt: Schon der Upload selbst wird kleiner. Wer vom Handy
aus ein 8-MB-Foto hochlaedt, wartet sonst am Mobilfunknetz.
⚠️ WAS BEWUSST NICHT ANGETASTET WIRD
Kleine PNGs bleiben PNG. Ein Logo mit durchsichtigem Hintergrund
wuerde als JPEG einen schwarzen Kasten bekommen -- JPEG kennt keine
Transparenz. Die Grenze liegt bei 400 KB: Darunter ist es
wahrscheinlich eine Grafik, darueber praktisch immer ein Foto.
===================================================================== */
const BILD_MAX_BREITE = 1600; // reicht fuer jede Anzeige auf dieser Seite
const BILD_QUALITAET = 0.86;
const PNG_GRENZE = 400 * 1024; // darunter: unangetastet lassen
async function bildVorbereiten(datei) {
try {
if (!datei || !datei.type.startsWith("image/")) return datei;
const istPng = datei.type === "image/png";
const bild = await new Promise((fertig, schief) => {
const b = new Image();
b.onload = () => fertig(b);
b.onerror = schief;
b.src = URL.createObjectURL(datei);
});
const zuBreit = bild.naturalWidth > BILD_MAX_BREITE;
const grossesPng = istPng && datei.size > PNG_GRENZE;
/* Nichts zu tun: kleines Bild, richtiges Format. Dann bleibt die
Datei unveraendert -- kein Qualitaetsverlust ohne Gegenwert. */
if (!zuBreit && !grossesPng) {
URL.revokeObjectURL(bild.src);
return datei;
}
const breite = Math.min(bild.naturalWidth, BILD_MAX_BREITE);
const hoehe = Math.round((bild.naturalHeight / bild.naturalWidth) * breite);
const flaeche = document.createElement("canvas");
flaeche.width = breite;
flaeche.height = hoehe;
const stift = flaeche.getContext("2d");
/* Ohne diese beiden Zeilen skaliert der Browser hart und die
Kanten franst aus. */
stift.imageSmoothingEnabled = true;
stift.imageSmoothingQuality = "high";
/* Grosse PNGs werden zu JPEG -- die haben keinen durchsichtigen
Hintergrund, sonst waeren sie nicht so gross. Sicherheitshalber
wird trotzdem weiss unterlegt, damit aus einer eventuellen
Transparenz kein schwarzer Kasten wird. */
if (grossesPng) {
stift.fillStyle = "#ffffff";
stift.fillRect(0, 0, breite, hoehe);
}
stift.drawImage(bild, 0, 0, breite, hoehe);
URL.revokeObjectURL(bild.src);
const typ = (grossesPng || datei.type === "image/jpeg") ? "image/jpeg" : datei.type;
const klotz = await new Promise((fertig) =>
flaeche.toBlob(fertig, typ, BILD_QUALITAET));
/* Wurde es NICHT kleiner, bleibt das Original. Bei bereits gut
komprimierten Dateien kann ein erneutes Kodieren sonst sogar
zulegen -- und Qualitaet kosten fuer nichts. */
if (!klotz || klotz.size >= datei.size) return datei;
const name = datei.name.replace(/\.[^.]+$/, "") +
(typ === "image/jpeg" ? ".jpg" : datei.name.slice(datei.name.lastIndexOf(".")));
return new File([klotz], name, { type: typ, lastModified: Date.now() });
} catch (e) {
/* Geht irgendetwas schief, wird das Original hochgeladen. Ein
fehlgeschlagenes Verkleinern darf niemals einen Upload
verhindern. */
return datei;
}
}
/* Nach aussen verfuegbar machen -- gleiches Vorgehen wie bei
window.vwBalkenSetzen im Webdesign-Bereich.
Grund ist die Pruefbarkeit: Die Funktion sitzt im gekapselten
Skriptbereich, ein Test erreicht sie sonst nicht. Und eine
Bildaufbereitung, die man nicht pruefen kann, prueft niemand --
dabei entscheidet sie darueber, ob ein 8-MB-Foto oder ein 200-KB-
Bild auf dem Server landet. */
window.bildVorbereiten = bildVorbereiten;
const REDEEM_LABELS = {
verfuegbar: "Verfügbar", eingeloest: "Eingelöst", wird_geprueft: "Wird geprüft", wird_vorbereitet: "Wird vorbereitet",
versandbereit: "Versandbereit", versendet: "Versendet", zugestellt: "Zugestellt", erhalten: "Erhalten",
@@ -1461,7 +1572,7 @@
statusEl.textContent = "Bild wird hochgeladen …";
statusEl.style.color = "";
const form = new FormData();
form.append("image", datei);
form.append("image", await bildVorbereiten(datei));
const session2 = window.AdminAuth.getSession();
try {
const res = await fetch(`${API_BASE_URL}/admin/events/upload-image`, {
@@ -1851,7 +1962,7 @@
statusEl.textContent = "Foto wird hochgeladen …";
const form = new FormData();
form.append("image", datei);
form.append("image", await bildVorbereiten(datei));
const session = window.AdminAuth.getSession();
try {
const res = await fetch(`${API_BASE_URL}/admin/testimonials/upload-avatar`, {
@@ -2023,7 +2134,7 @@
statusEl.textContent = "Foto wird hochgeladen …";
statusEl.style.color = "";
const form = new FormData();
form.append("image", datei);
form.append("image", await bildVorbereiten(datei));
const session = window.AdminAuth.getSession();
try {
const res = await fetch(`${API_BASE_URL}/admin/team/upload-photo`, {