From 431792e8d990d7fe9ca45e6f03f02e27eb9dcaef Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 3 Jun 2026 16:06:05 +0200 Subject: [PATCH] =?UTF-8?q?JpgToPdfModal:=20PDF-Gr=C3=B6=C3=9Fe=20massiv?= =?UTF-8?q?=20reduziert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/todo.md | 16 ++++++++ frontend/src/components/ui/JpgToPdfModal.tsx | 39 ++++++++++++++++---- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/docs/todo.md b/docs/todo.md index 179fd564..9510a579 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,22 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ 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** - Globaler Error-Handler in `index.ts:461` matcht `/sind erlaubt|nicht erlaubt/i` und mappt auf 415. Meine 70.1- diff --git a/frontend/src/components/ui/JpgToPdfModal.tsx b/frontend/src/components/ui/JpgToPdfModal.tsx index c1fb141f..07dba80d 100644 --- a/frontend/src/components/ui/JpgToPdfModal.tsx +++ b/frontend/src/components/ui/JpgToPdfModal.tsx @@ -250,12 +250,35 @@ export default function JpgToPdfModal({ for (let i = 0; i < images.length; i++) { const item = images[i]; - const img = await loadImage(item.dataUrl); - const canvas = renderImageToCanvas(img, item); - const jpegDataUrl = canvas.toDataURL('image/jpeg', 1.0); + 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 canvas = renderImageToCanvas(img, item); + imageData = canvas.toDataURL('image/jpeg', 0.95); + imageFormat = 'JPEG'; + srcW = canvas.width; + srcH = canvas.height; + } const orientation: 'portrait' | 'landscape' = - canvas.width > canvas.height ? 'landscape' : 'portrait'; + srcW > srcH ? 'landscape' : 'portrait'; const pageW = orientation === 'landscape' ? a4h : a4w; const pageH = orientation === 'landscape' ? a4w : a4h; @@ -268,13 +291,13 @@ export default function JpgToPdfModal({ const margin = 5; const maxW = pageW - 2 * margin; const maxH = pageH - 2 * margin; - const ratio = Math.min(maxW / canvas.width, maxH / canvas.height); - const w = canvas.width * ratio; - const h = canvas.height * ratio; + const ratio = Math.min(maxW / srcW, maxH / srcH); + const w = srcW * ratio; + const h = srcH * ratio; const x = (pageW - w) / 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');