From d8a3e9de86105fe145d20bbd0d344fa063ada424 Mon Sep 17 00:00:00 2001 From: Kenearos Date: Tue, 12 May 2026 00:13:57 +0200 Subject: [PATCH] feat(image-import): add compressImage with canvas resize + JPEG re-encode --- image-import.js | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ test-suite.js | 46 ++++++++++++++++++++++++++++++++++++++++++++ test.html | 1 + 3 files changed, 98 insertions(+) diff --git a/image-import.js b/image-import.js index cfd0f5b..f1ffdf3 100644 --- a/image-import.js +++ b/image-import.js @@ -11,6 +11,57 @@ class ImageImporter { this.session = null; this.abortController = null; } + + /** + * Resize image so the longest edge is <= 2048 px, re-encode as JPEG q=0.85. + * @param {File|Blob} file + * @returns {Promise<{blob: Blob, dataUrl: string, width: number, height: number}>} + */ + async compressImage(file) { + const objUrl = URL.createObjectURL(file); + try { + const img = await new Promise((resolve, reject) => { + const i = new Image(); + i.onload = () => resolve(i); + i.onerror = () => reject(new Error('Bild konnte nicht geladen werden')); + i.src = objUrl; + }); + + const longest = Math.max(img.width, img.height); + let newW = img.width; + let newH = img.height; + if (longest > 2048) { + const scale = 2048 / longest; + newW = Math.round(img.width * scale); + newH = Math.round(img.height * scale); + } + + const canvas = document.createElement('canvas'); + canvas.width = newW; + canvas.height = newH; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, newW, newH); + + const blob = await new Promise((resolve, reject) => { + canvas.toBlob( + (b) => b ? resolve(b) : reject(new Error('toBlob fehlgeschlagen')), + 'image/jpeg', + 0.85 + ); + }); + + const dataUrl = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(new Error('FileReader fehlgeschlagen')); + reader.readAsDataURL(blob); + }); + + return { blob, dataUrl, width: newW, height: newH }; + } finally { + URL.revokeObjectURL(objUrl); + } + } } // Make available globally diff --git a/test-suite.js b/test-suite.js index 8b8f4cb..d00278f 100644 --- a/test-suite.js +++ b/test-suite.js @@ -527,6 +527,52 @@ runner.test('Storage API Key: clearAll laesst API-Key unberuehrt', (t) => { storage.clearApiKey(); }); +// ============================================================================ +// ImageImporter Tests - Preprocessing (Feature A) +// ============================================================================ + +/** + * Helper: build a synthetic image File from a canvas. + */ +async function makeTestImageFile(width, height, mime = 'image/png') { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#3366cc'; + ctx.fillRect(0, 0, width, height); + ctx.fillStyle = '#ffffff'; + ctx.font = '32px sans-serif'; + ctx.fillText('TEST', 20, 50); + const blob = await new Promise(res => canvas.toBlob(res, mime)); + return new File([blob], 'test.png', { type: mime }); +} + +runner.test('Preprocess: 4000x3000 wird auf laengste Kante 2048 skaliert', async (t) => { + const importer = new ImageImporter(null); + const file = await makeTestImageFile(4000, 3000); + const result = await importer.compressImage(file); + t.assertEqual(result.width, 2048, 'Breite sollte 2048 sein'); + t.assertEqual(result.height, 1536, 'Hoehe sollte 1536 sein (Seitenverhaeltnis erhalten)'); + t.assertTrue(result.dataUrl.startsWith('data:image/jpeg;base64,'), 'dataUrl-Prefix korrekt'); +}); + +runner.test('Preprocess: 800x600 bleibt unveraendert (kein Upscale)', async (t) => { + const importer = new ImageImporter(null); + const file = await makeTestImageFile(800, 600); + const result = await importer.compressImage(file); + t.assertEqual(result.width, 800, 'Breite unveraendert'); + t.assertEqual(result.height, 600, 'Hoehe unveraendert'); +}); + +runner.test('Preprocess: Output ist immer JPEG', async (t) => { + const importer = new ImageImporter(null); + const file = await makeTestImageFile(500, 500, 'image/png'); + const result = await importer.compressImage(file); + t.assertTrue(result.dataUrl.startsWith('data:image/jpeg;base64,'), 'Output ist JPEG'); + t.assertTrue(result.dataUrl.length > 1000, 'Output-Laenge > 1KB'); +}); + // ============================================================================ // Display Functions // ============================================================================ diff --git a/test.html b/test.html index 763e9de..34e051c 100644 --- a/test.html +++ b/test.html @@ -142,6 +142,7 @@ +