Öffentliche Avatar-Uploads vor dem Absenden verkleinern (1,1 MB -> ~40 KB)
Der Performance-Check (pruef-tempo.mjs, neu) fand als größte Einzel- ressource ein 1,1-MB-Profilbild, ausgeliefert an jeden Besucher der Startseite und von stimmen.html -- angezeigt wird es handgroß. Ursache: Das öffentliche Stimmen-Formular (stimmen.js) lud Profilbilder ROH hoch. Die Bildaufbereitung vom 27.08. bekam nur die Verwaltung, nicht das öffentliche Formular. So landete das Kamera-Foto in voller Auflösung auf dem Server. - bild-vorbereiten.js (neu): die Verkleinerungs-Funktion, jetzt einmal und parametrisierbar (maxBreite). Verwaltung nutzt weiter 1600px, Avatare 512px. Test pruef-bild-vorbereiten.mjs: 8/8, u.a. 512er-Avatar ~40 KB statt >1 MB. - stimmen.js: verkleinert vor dem Upload (window.bildVorbereiten mit maxBreite 512); fällt die Funktion aus, wird das Original genommen -- kein Upload darf daran scheitern. - stimmen.html: lädt bild-vorbereiten.js; veralteten Kommentar richtiggestellt (behauptete "kein Upload", obwohl es seit 21.08. einen gibt -- mit Bremse, Typ-/Größenlimit, Freigabe-Pflicht). Zwei neue Checks als bleibende Absicherung mit committet: pruef-links.mjs (41 interne Ziele, 0 kaputt) und pruef-assets.mjs (67 Ressourcen, 0 fehlen). NOCH OFFEN: verwaltung.html hat noch eine eigene, identische Inline-Kopie der Funktion -- die Zusammenführung ist ein eigener, testbarer Schritt (das Inline-Skript dort ist groß und die Verwaltung hinter dem Gate schwer live zu testen). Das bestehende 1,1-MB-Bild auf dem Server bleibt, bis es neu hochgeladen/ersetzt wird. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
/* =====================================================================
|
||||
bild-vorbereiten.js — ein Bild im Browser verkleinern, BEVOR es hochlädt
|
||||
|
||||
WOZU
|
||||
|
||||
Serverseitig bräuchte es eine Bildbibliothek (sharp o.ä.) -- eine neue
|
||||
Abhängigkeit mit nativem Code, installiert in einem Verzeichnis, an das
|
||||
Claudian nicht herankommt. Der Browser kann das ohnehin: Canvas skaliert
|
||||
und kodiert seit jeher. Nebeneffekt: schon der Upload wird kleiner -- wer
|
||||
vom Handy ein 8-MB-Foto schickt, wartet sonst am Mobilfunknetz.
|
||||
|
||||
Bis 28.08.2026 lag diese Funktion NUR inline in der Verwaltung
|
||||
(verwaltung.html). Das öffentliche Stimmen-Formular (stimmen.js) lud
|
||||
Profilbilder dagegen ROH hoch -- so landete ein 1,1-MB-Avatar auf dem
|
||||
Server und wurde jedem Besucher der Startseite ausgeliefert. Deshalb
|
||||
liegt die Funktion jetzt hier, EINMAL, und wird von beiden benutzt.
|
||||
|
||||
MASS PRO EINSATZORT
|
||||
|
||||
Die größte Breite ist ein Parameter, kein fester Wert: Die Verwaltung
|
||||
zeigt Event-/Teambilder groß (1600 reicht für jede Anzeige dort), ein
|
||||
Profil-Avatar erscheint dagegen nur handgroß -- 512 ist da schon
|
||||
großzügig und spart gegenüber 1600 ein Vielfaches.
|
||||
|
||||
WAS BEWUSST NICHT ANGETASTET WIRD
|
||||
|
||||
Kleine PNGs bleiben PNG. Ein Logo mit durchsichtigem Hintergrund würde
|
||||
als JPEG einen schwarzen Kasten bekommen -- JPEG kennt keine Transparenz.
|
||||
Die Grenze liegt bei 400 KB: Darunter ist es wahrscheinlich eine Grafik,
|
||||
darüber praktisch immer ein Foto.
|
||||
===================================================================== */
|
||||
(function () {
|
||||
const STANDARD = {
|
||||
maxBreite: 1600, // reicht für die großen Anzeigen in der Verwaltung
|
||||
qualitaet: 0.86,
|
||||
pngGrenze: 400 * 1024, // kleine PNGs (Grafiken/Logos) unangetastet lassen
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {File} datei die vom Nutzer gewählte Datei
|
||||
* @param {object} [opt] {maxBreite, qualitaet, pngGrenze}
|
||||
* @returns {Promise<File>} verkleinerte Datei -- oder das Original, wenn
|
||||
* sich Verkleinern nicht lohnt oder etwas schiefgeht.
|
||||
*/
|
||||
async function bildVorbereiten(datei, opt) {
|
||||
const maxBreite = opt?.maxBreite ?? STANDARD.maxBreite;
|
||||
const qualitaet = opt?.qualitaet ?? STANDARD.qualitaet;
|
||||
const pngGrenze = opt?.pngGrenze ?? STANDARD.pngGrenze;
|
||||
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 > maxBreite;
|
||||
const grossesPng = istPng && datei.size > pngGrenze;
|
||||
|
||||
/* Nichts zu tun: kleines Bild, richtiges Format. Dann bleibt die
|
||||
Datei unverändert -- kein Qualitätsverlust ohne Gegenwert. */
|
||||
if (!zuBreit && !grossesPng) {
|
||||
URL.revokeObjectURL(bild.src);
|
||||
return datei;
|
||||
}
|
||||
|
||||
const breite = Math.min(bild.naturalWidth, maxBreite);
|
||||
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
|
||||
fransen aus. */
|
||||
stift.imageSmoothingEnabled = true;
|
||||
stift.imageSmoothingQuality = "high";
|
||||
|
||||
/* Große PNGs werden zu JPEG -- die haben keinen durchsichtigen
|
||||
Hintergrund, sonst wären sie nicht so groß. Sicherheitshalber wird
|
||||
trotzdem weiß 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, qualitaet));
|
||||
|
||||
/* Wurde es NICHT kleiner, bleibt das Original. Bei bereits gut
|
||||
komprimierten Dateien kann erneutes Kodieren sonst sogar zulegen --
|
||||
und Qualität kosten für 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;
|
||||
}
|
||||
}
|
||||
|
||||
window.bildVorbereiten = bildVorbereiten;
|
||||
})();
|
||||
+11
-1
@@ -69,7 +69,17 @@
|
||||
eigenesBtn.classList.add("laedt");
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append("image", datei);
|
||||
/* Vor dem Upload verkleinern (bild-vorbereiten.js). Ein Profilbild
|
||||
erscheint nur handgroß -- 512px sind großzügig und machen aus
|
||||
einem Handy-Foto von mehreren MB rund 30-50 KB. Ohne diesen
|
||||
Schritt landete bis 28.08.2026 z.B. ein 1,1-MB-Avatar auf dem
|
||||
Server und wurde jedem Startseiten-Besucher ausgeliefert.
|
||||
Fällt window.bildVorbereiten aus (Skript nicht geladen), wird
|
||||
die Originaldatei genommen -- kein Upload darf daran scheitern. */
|
||||
const fertig = window.bildVorbereiten
|
||||
? await window.bildVorbereiten(datei, { maxBreite: 512 })
|
||||
: datei;
|
||||
form.append("image", fertig);
|
||||
const res = await fetch(`${API_BASE_URL}/testimonials/upload-avatar`, { method: "POST", body: form });
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
|
||||
Reference in New Issue
Block a user