JpgToPdfModal: Bilder auf 2400px runterskalieren
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<HTMLImageElement> {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user