JpgToPdfModal: PDF-Größe massiv reduziert

Stage-Bug: 2 Handy-JPGs à 2 MB → PDF >10 MB → Multer 413. Ursache:
Canvas-Re-Encode mit JPEG-Quality 1.0 blies jedes Bild auf 8-15 MB
auf (Quality 100 % ≠ "identisch zum Original", sondern "möglichst
viele Bits pro Pixel" – ein schon JPEG-komprimiertes Smartphone-
Foto wird so künstlich 4-8× größer).

Fix 1: Wenn Rotation/Flip unverändert (Standardfall), Original-
DataURL 1:1 in die PDF einbetten – kein Canvas-Roundtrip, keine
Quality-Aufblähung. 2-MB-JPEG bleibt 2 MB. Format-Detection per
data:image/png-Prefix (PNG vs JPEG).

Fix 2: Bei Transformation toDataURL('image/jpeg', 0.95) statt 1.0.
Visuell identisch für Foto-Inhalte, 50-70 % kleiner.

Kombiniert: 2 untransformierte Handy-Fotos ≈ 4 MB PDF (vorher
16-30 MB), 2 gedrehte ≈ 5-8 MB.
This commit is contained in:
2026-06-03 16:06:05 +02:00
parent d5dd3f5e7f
commit 431792e8d9
2 changed files with 47 additions and 8 deletions
+16
View File
@@ -97,6 +97,22 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt ## ✅ Erledigt
- [x] **🆕 JpgToPdfModal: PDF-Größe drastisch reduziert (Original-Bytes + Quality 0.95)**
- Stage-Bug: 2 Handy-JPGs à 2 MB → PDF >10 MB → Multer 413. Ursache:
Canvas-Re-Encode mit JPEG-Quality 1.0 blies jedes Bild auf 8-15 MB
auf (Quality 100 % heißt nicht „identisch zum Original", sondern
„möglichst viele Bits pro Pixel" ein schon JPEG-komprimiertes
Smartphone-Foto wird so künstlich 4-8× größer).
- **Fix 1:** Wenn Rotation/Flip unverändert (Standardfall), wird die
Original-DataURL 1:1 in die PDF eingebettet kein Canvas-Roundtrip,
keine Quality-Aufblähung. 2-MB-JPEG bleibt 2 MB. Funktioniert für
JPEG und PNG (Format-Detection per `data:image/png`-Prefix).
- **Fix 2:** Bei Transformation: `toDataURL('image/jpeg', 0.95)` statt
`1.0`. Visuell identisch für Foto-Inhalte (Adobe-Lightroom-Default),
aber 50-70 % kleiner.
- Kombiniert: 2 untransformierte Handy-Fotos ≈ 4 MB PDF (vorher
16-30 MB), 2 gedrehte ≈ 5-8 MB.
- [x] **🔒 Pentest 70.2 (LOW): falscher 500 statt 415 bei verbotenem MIME-Type** - [x] **🔒 Pentest 70.2 (LOW): falscher 500 statt 415 bei verbotenem MIME-Type**
- Globaler Error-Handler in `index.ts:461` matcht - Globaler Error-Handler in `index.ts:461` matcht
`/sind erlaubt|nicht erlaubt/i` und mappt auf 415. Meine 70.1- `/sind erlaubt|nicht erlaubt/i` und mappt auf 415. Meine 70.1-
+29 -6
View File
@@ -250,12 +250,35 @@ export default function JpgToPdfModal({
for (let i = 0; i < images.length; i++) { for (let i = 0; i < images.length; i++) {
const item = images[i]; const item = images[i];
const untouched = item.rotation === 0 && !item.flipH && !item.flipV;
// Fix 1: ungedrehte/ungespiegelte Bilder bekommen ihre Original-Bytes
// direkt in die PDF eingebettet kein Canvas-Re-Encode, kein
// Quality-Aufblasen. Ein 2-MB-JPEG bleibt 2 MB statt 8-15 MB zu werden.
// Fix 2: wenn doch transformiert wird (Rotation/Flip), Canvas mit
// Quality 0.95 statt 1.0 visuell identisch für Foto-Inhalte, aber
// 50-70 % kleiner.
let imageData: string;
let imageFormat: 'JPEG' | 'PNG';
let srcW: number;
let srcH: number;
if (untouched) {
imageData = item.dataUrl;
imageFormat = item.dataUrl.startsWith('data:image/png') ? 'PNG' : 'JPEG';
srcW = item.naturalWidth;
srcH = item.naturalHeight;
} else {
const img = await loadImage(item.dataUrl); const img = await loadImage(item.dataUrl);
const canvas = renderImageToCanvas(img, item); const canvas = renderImageToCanvas(img, item);
const jpegDataUrl = canvas.toDataURL('image/jpeg', 1.0); imageData = canvas.toDataURL('image/jpeg', 0.95);
imageFormat = 'JPEG';
srcW = canvas.width;
srcH = canvas.height;
}
const orientation: 'portrait' | 'landscape' = const orientation: 'portrait' | 'landscape' =
canvas.width > canvas.height ? 'landscape' : 'portrait'; srcW > srcH ? 'landscape' : 'portrait';
const pageW = orientation === 'landscape' ? a4h : a4w; const pageW = orientation === 'landscape' ? a4h : a4w;
const pageH = orientation === 'landscape' ? a4w : a4h; const pageH = orientation === 'landscape' ? a4w : a4h;
@@ -268,13 +291,13 @@ export default function JpgToPdfModal({
const margin = 5; const margin = 5;
const maxW = pageW - 2 * margin; const maxW = pageW - 2 * margin;
const maxH = pageH - 2 * margin; const maxH = pageH - 2 * margin;
const ratio = Math.min(maxW / canvas.width, maxH / canvas.height); const ratio = Math.min(maxW / srcW, maxH / srcH);
const w = canvas.width * ratio; const w = srcW * ratio;
const h = canvas.height * ratio; const h = srcH * ratio;
const x = (pageW - w) / 2; const x = (pageW - w) / 2;
const y = (pageH - h) / 2; const y = (pageH - h) / 2;
pdf.addImage(jpegDataUrl, 'JPEG', x, y, w, h, undefined, 'SLOW'); pdf.addImage(imageData, imageFormat, x, y, w, h, undefined, 'SLOW');
} }
const blob = pdf!.output('blob'); const blob = pdf!.output('blob');