From 523eab30d517f73c1e2b34ada156e611c571688f Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 3 Jun 2026 18:29:04 +0200 Subject: [PATCH] JpgToPdfModal: Bilder auf 2400px runterskalieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage: 2 Handy-JPGs → 23 MB PDF. Smartphone-Fotos haben 4000-6000 px Kante, das macht auch ohne Re-Encode 5-10 MB pro Bild → PDF wird riesig. Beim Hinzufügen werden Bilder jetzt auf max 2400 px lange Kante runterskaliert (~290 DPI auf A4 = Druckqualität) und als JPEG mit Quality 0.92 (Lightroom-Default) persistiert. Vorschau, Rotation/ Flip und PDF-Embed laufen alle auf dem skalierten Bild. Erwartete Größe: 2 Handy-Fotos ≈ 1-2 MB PDF. --- docs/todo.md | 11 +++++ frontend/src/components/ui/JpgToPdfModal.tsx | 44 +++++++++++++++++--- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/docs/todo.md b/docs/todo.md index 7d7c8e33..c7e7729b 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,17 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🐞 JpgToPdfModal: PDF blieb trotz vorherigem Fix bei 20+ MB** + - Stage-Test: 2 Handy-JPGs → 23 MB PDF. Ursache: Smartphone-Fotos + haben 4000-6000 px Kante (24 MP), das vergrößert die JPEG-Datei + auch ohne Re-Encode auf 5-10 MB pro Bild. + - Fix: Bilder **beim Hinzufügen** auf max. 2400 px lange Kante + runterskaliert (~290 DPI auf A4 = Druckqualität) und als JPEG mit + Quality 0.92 (Lightroom-Default, kein wahrnehmbarer Unterschied) + persistiert. Vorschau-Thumbnail, Rotation/Flip und finaler + PDF-Embed laufen alle auf dem skalierten Bild. + - Erwartete Größe: 2 Handy-Fotos ≈ 1-2 MB PDF (statt 23 MB). + - [x] **🆕 Kunden-Detail-Tabs: Pro-Tab-Link „in neuem Tab öffnen"** - `Tabs`-Komponente um optionalen Prop `tabHrefBuilder(tabId)` erweitert. Wenn gesetzt, erscheint neben jedem Tab-Label ein kleines diff --git a/frontend/src/components/ui/JpgToPdfModal.tsx b/frontend/src/components/ui/JpgToPdfModal.tsx index 07dba80d..f421dfbb 100644 --- a/frontend/src/components/ui/JpgToPdfModal.tsx +++ b/frontend/src/components/ui/JpgToPdfModal.tsx @@ -37,6 +37,14 @@ interface JpgToPdfModalProps { const MAX_IMAGES = 50; const MAX_IMAGE_BYTES = 25 * 1024 * 1024; +// Smartphone-Fotos haben oft 4000-6000 px Kante. Bei JPEG-Quality +// 0.95 sind das 5-10 MB pro Seite, zwei Bilder = >10 MB PDF. +// 2400 px lange Kante entspricht ~290 DPI auf A4 (Druckqualität) und +// reduziert die Pixelmenge auf 25-36 % vom Original → PDF wird +// drastisch kleiner, sichtbarer Unterschied praktisch null. +const MAX_DIMENSION = 2400; +const EMBED_QUALITY = 0.92; + function makeId() { return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; } @@ -59,6 +67,23 @@ function loadImage(dataUrl: string): Promise { }); } +function downscaleIfNeeded(image: HTMLImageElement): HTMLCanvasElement | null { + const w = image.naturalWidth; + const h = image.naturalHeight; + if (w <= MAX_DIMENSION && h <= MAX_DIMENSION) return null; + const scale = MAX_DIMENSION / Math.max(w, h); + const newW = Math.round(w * scale); + const newH = Math.round(h * scale); + const canvas = document.createElement('canvas'); + canvas.width = newW; + canvas.height = newH; + const ctx = canvas.getContext('2d'); + if (!ctx) return null; + ctx.imageSmoothingQuality = 'high'; + ctx.drawImage(image, 0, 0, newW, newH); + return canvas; +} + function renderImageToCanvas(image: HTMLImageElement, item: ImageItem): HTMLCanvasElement { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); @@ -123,13 +148,22 @@ export default function JpgToPdfModal({ break; } try { - const dataUrl = await readFileAsDataUrl(file); - const img = await loadImage(dataUrl); + const rawDataUrl = await readFileAsDataUrl(file); + const img = await loadImage(rawDataUrl); + // Beim Hinzufügen direkt auf MAX_DIMENSION runterskalieren, damit + // die Vorschau, das Rendern in der PDF und die finale Dateigröße + // alle auf vernünftigen Pixelmaßen arbeiten. + const downscaled = downscaleIfNeeded(img); + const dataUrl = downscaled + ? downscaled.toDataURL('image/jpeg', EMBED_QUALITY) + : rawDataUrl; + const finalW = downscaled ? downscaled.width : img.naturalWidth; + const finalH = downscaled ? downscaled.height : img.naturalHeight; added.push({ id: makeId(), dataUrl, - naturalWidth: img.naturalWidth, - naturalHeight: img.naturalHeight, + naturalWidth: finalW, + naturalHeight: finalH, rotation: 0, flipH: false, flipV: false, @@ -271,7 +305,7 @@ export default function JpgToPdfModal({ } else { const img = await loadImage(item.dataUrl); const canvas = renderImageToCanvas(img, item); - imageData = canvas.toDataURL('image/jpeg', 0.95); + imageData = canvas.toDataURL('image/jpeg', EMBED_QUALITY); imageFormat = 'JPEG'; srcW = canvas.width; srcH = canvas.height;