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
|
## ✅ 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"**
|
- [x] **🆕 Kunden-Detail-Tabs: Pro-Tab-Link „in neuem Tab öffnen"**
|
||||||
- `Tabs`-Komponente um optionalen Prop `tabHrefBuilder(tabId)` erweitert.
|
- `Tabs`-Komponente um optionalen Prop `tabHrefBuilder(tabId)` erweitert.
|
||||||
Wenn gesetzt, erscheint neben jedem Tab-Label ein kleines
|
Wenn gesetzt, erscheint neben jedem Tab-Label ein kleines
|
||||||
|
|||||||
@@ -37,6 +37,14 @@ interface JpgToPdfModalProps {
|
|||||||
const MAX_IMAGES = 50;
|
const MAX_IMAGES = 50;
|
||||||
const MAX_IMAGE_BYTES = 25 * 1024 * 1024;
|
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() {
|
function makeId() {
|
||||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
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 {
|
function renderImageToCanvas(image: HTMLImageElement, item: ImageItem): HTMLCanvasElement {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
@@ -123,13 +148,22 @@ export default function JpgToPdfModal({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const dataUrl = await readFileAsDataUrl(file);
|
const rawDataUrl = await readFileAsDataUrl(file);
|
||||||
const img = await loadImage(dataUrl);
|
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({
|
added.push({
|
||||||
id: makeId(),
|
id: makeId(),
|
||||||
dataUrl,
|
dataUrl,
|
||||||
naturalWidth: img.naturalWidth,
|
naturalWidth: finalW,
|
||||||
naturalHeight: img.naturalHeight,
|
naturalHeight: finalH,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
flipH: false,
|
flipH: false,
|
||||||
flipV: false,
|
flipV: false,
|
||||||
@@ -271,7 +305,7 @@ export default function JpgToPdfModal({
|
|||||||
} else {
|
} else {
|
||||||
const img = await loadImage(item.dataUrl);
|
const img = await loadImage(item.dataUrl);
|
||||||
const canvas = renderImageToCanvas(img, item);
|
const canvas = renderImageToCanvas(img, item);
|
||||||
imageData = canvas.toDataURL('image/jpeg', 0.95);
|
imageData = canvas.toDataURL('image/jpeg', EMBED_QUALITY);
|
||||||
imageFormat = 'JPEG';
|
imageFormat = 'JPEG';
|
||||||
srcW = canvas.width;
|
srcW = canvas.width;
|
||||||
srcH = canvas.height;
|
srcH = canvas.height;
|
||||||
|
|||||||
Reference in New Issue
Block a user