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:
+114
-3
@@ -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`, {
|
||||
|
||||
Reference in New Issue
Block a user