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:
@@ -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-
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user