Fix image uploads and reduce flyer request sizes
This commit is contained in:
+13
-3
@@ -1044,17 +1044,27 @@ def save_image(upload: UploadFile, destination_dir: str, url_prefix: str):
|
|||||||
if image.width * image.height > MAX_IMAGE_PIXELS:
|
if image.width * image.height > MAX_IMAGE_PIXELS:
|
||||||
raise ValueError("Bildauflösung zu groß")
|
raise ValueError("Bildauflösung zu groß")
|
||||||
image.thumbnail((2400, 2400))
|
image.thumbnail((2400, 2400))
|
||||||
|
has_alpha = image.mode in {"RGBA", "LA"} or (
|
||||||
|
image.mode == "P" and "transparency" in image.info
|
||||||
|
)
|
||||||
if image.mode not in {"RGB", "RGBA"}:
|
if image.mode not in {"RGB", "RGBA"}:
|
||||||
image = image.convert("RGBA" if "transparency" in image.info else "RGB")
|
image = image.convert("RGBA" if has_alpha else "RGB")
|
||||||
output = BytesIO()
|
output = BytesIO()
|
||||||
image.save(output, format="WEBP", quality=88, method=6)
|
# Nicht jedes Browser-/Proxy-Setup verarbeitet serverseitig
|
||||||
|
# erzeugte WebP-Dateien zuverlässig. Nach der Validierung bleiben
|
||||||
|
# transparente Bilder PNG, normale Bilder werden als JPEG gespeichert.
|
||||||
|
output_format = "PNG" if image.mode == "RGBA" else "JPEG"
|
||||||
|
if output_format == "PNG":
|
||||||
|
image.save(output, format=output_format, optimize=True)
|
||||||
|
else:
|
||||||
|
image.save(output, format=output_format, quality=88, optimize=True)
|
||||||
safe_contents = output.getvalue()
|
safe_contents = output.getvalue()
|
||||||
except (ValueError, OSError, UnidentifiedImageError, Image.DecompressionBombError):
|
except (ValueError, OSError, UnidentifiedImageError, Image.DecompressionBombError):
|
||||||
return None, HTMLResponse(
|
return None, HTMLResponse(
|
||||||
"Die Datei ist kein gültiges oder unterstütztes Bild.", status_code=400
|
"Die Datei ist kein gültiges oder unterstütztes Bild.", status_code=400
|
||||||
)
|
)
|
||||||
|
|
||||||
filename = str(uuid.uuid4()) + ".webp"
|
filename = str(uuid.uuid4()) + (".png" if output_format == "PNG" else ".jpg")
|
||||||
destination = os.path.join(destination_dir, filename)
|
destination = os.path.join(destination_dir, filename)
|
||||||
with open(destination, "wb") as file:
|
with open(destination, "wb") as file:
|
||||||
file.write(safe_contents)
|
file.write(safe_contents)
|
||||||
|
|||||||
@@ -133,6 +133,45 @@
|
|||||||
</main>
|
</main>
|
||||||
{% if can_edit_details %}
|
{% if can_edit_details %}
|
||||||
<script>
|
<script>
|
||||||
|
const editConcertForm = document.querySelector('form[enctype="multipart/form-data"]');
|
||||||
|
const editFlyerInput = editConcertForm?.querySelector('input[name="flyer"]');
|
||||||
|
async function optimizeEditFlyer(file) {
|
||||||
|
const bitmap = await createImageBitmap(file);
|
||||||
|
const maxSide = 1800;
|
||||||
|
const scale = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height));
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
|
||||||
|
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
|
||||||
|
canvas.getContext("2d").drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
||||||
|
bitmap.close();
|
||||||
|
let quality = 0.86;
|
||||||
|
let blob;
|
||||||
|
do {
|
||||||
|
blob = await new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", quality));
|
||||||
|
quality -= 0.08;
|
||||||
|
} while (blob && blob.size > 900 * 1024 && quality >= 0.5);
|
||||||
|
if (!blob) throw new Error("Bild konnte nicht verarbeitet werden");
|
||||||
|
return new File([blob], "flyer.jpg", {type: "image/jpeg"});
|
||||||
|
}
|
||||||
|
|
||||||
|
editConcertForm?.addEventListener("submit", async event => {
|
||||||
|
if (editConcertForm.dataset.flyerOptimized === "true") return;
|
||||||
|
const file = editFlyerInput?.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
event.preventDefault();
|
||||||
|
try {
|
||||||
|
const transfer = new DataTransfer();
|
||||||
|
transfer.items.add(await optimizeEditFlyer(file));
|
||||||
|
editFlyerInput.files = transfer.files;
|
||||||
|
editConcertForm.dataset.flyerOptimized = "true";
|
||||||
|
editConcertForm.requestSubmit();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Flyer optimization failed:", error);
|
||||||
|
editConcertForm.dataset.flyerOptimized = "true";
|
||||||
|
editConcertForm.requestSubmit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const eventType = document.getElementById("event-type");
|
const eventType = document.getElementById("event-type");
|
||||||
const parentEventField = document.getElementById("parent-event-field");
|
const parentEventField = document.getElementById("parent-event-field");
|
||||||
const parentEventId = document.getElementById("parent-event-id");
|
const parentEventId = document.getElementById("parent-event-id");
|
||||||
|
|||||||
@@ -877,6 +877,48 @@ flyerInput.addEventListener(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Große Flyer vor dem Absenden verkleinern. Dadurch bleibt der Multipart-
|
||||||
|
// Request unter typischen Proxy-Limits und der Server muss keine riesigen
|
||||||
|
// Originaldateien verarbeiten.
|
||||||
|
const concertForm = flyerInput.form;
|
||||||
|
async function optimizeFlyerUpload(file) {
|
||||||
|
const bitmap = await createImageBitmap(file);
|
||||||
|
const maxSide = 1800;
|
||||||
|
const scale = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height));
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
|
||||||
|
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
||||||
|
bitmap.close();
|
||||||
|
let quality = 0.86;
|
||||||
|
let blob;
|
||||||
|
do {
|
||||||
|
blob = await new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", quality));
|
||||||
|
quality -= 0.08;
|
||||||
|
} while (blob && blob.size > 900 * 1024 && quality >= 0.5);
|
||||||
|
if (!blob) throw new Error("Bild konnte nicht verarbeitet werden");
|
||||||
|
return new File([blob], "flyer.jpg", {type: "image/jpeg"});
|
||||||
|
}
|
||||||
|
|
||||||
|
concertForm.addEventListener("submit", async event => {
|
||||||
|
if (concertForm.dataset.flyerOptimized === "true") return;
|
||||||
|
const file = flyerInput.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
event.preventDefault();
|
||||||
|
try {
|
||||||
|
const transfer = new DataTransfer();
|
||||||
|
transfer.items.add(await optimizeFlyerUpload(file));
|
||||||
|
flyerInput.files = transfer.files;
|
||||||
|
concertForm.dataset.flyerOptimized = "true";
|
||||||
|
concertForm.requestSubmit();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Flyer optimization failed:", error);
|
||||||
|
concertForm.dataset.flyerOptimized = "true";
|
||||||
|
concertForm.requestSubmit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
HTML ESCAPING
|
HTML ESCAPING
|
||||||
|
|||||||
@@ -213,10 +213,10 @@ if (profileForm) {
|
|||||||
context.imageSmoothingQuality = "high";
|
context.imageSmoothingQuality = "high";
|
||||||
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
||||||
bitmap.close();
|
bitmap.close();
|
||||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, "image/webp", .86));
|
const blob = await new Promise(resolve => canvas.toBlob(resolve, "image/jpeg", .88));
|
||||||
if (!blob) throw new Error("Bild konnte nicht verarbeitet werden");
|
if (!blob) throw new Error("Bild konnte nicht verarbeitet werden");
|
||||||
const transfer = new DataTransfer();
|
const transfer = new DataTransfer();
|
||||||
transfer.items.add(new File([blob], "profilbild.webp", {type: "image/webp"}));
|
transfer.items.add(new File([blob], "profilbild.jpg", {type: "image/jpeg"}));
|
||||||
input.files = transfer.files;
|
input.files = transfer.files;
|
||||||
profileForm.dataset.optimized = "true";
|
profileForm.dataset.optimized = "true";
|
||||||
status.textContent = `Optimiert: ${Math.ceil(blob.size / 1024)} KB – wird hochgeladen …`;
|
status.textContent = `Optimiert: ${Math.ceil(blob.size / 1024)} KB – wird hochgeladen …`;
|
||||||
|
|||||||
Reference in New Issue
Block a user