import sharp from 'sharp'; import { mkdtempSync, mkdirSync, rmSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { renderImageVersion, resolveEditableImage, normalizeEditorOperations } from '../lib/image-editor.mjs'; const temp = mkdtempSync(join(tmpdir(), 'vendoo-slice31-')); const uploadRoot = join(temp, 'uploads'); const sourceFolder = join(uploadRoot, 'verification'); mkdirSync(sourceFolder, { recursive: true }); const sourcePath = join(sourceFolder, 'original.png'); try { await sharp({ create: { width: 960, height: 720, channels: 4, background: { r: 225, g: 213, b: 192, alpha: 1 } }, }).composite([{ input: Buffer.from(''), }]).png().toFile(sourcePath); const normalized = normalizeEditorOperations({ rotation: 90, straighten: 2.5, flip_h: true, crop: { x: 0.08, y: 0.1, width: 0.84, height: 0.76 }, exposure: 0.3, brightness: 12, contrast: 18, highlights: -24, shadows: 28, temperature: 16, saturation: 14, sharpness: 38, resize: { width: 640, height: 640 }, remove_background: true, watermark: { enabled: true, text: 'Vendoo Test', position: 'bottom-right', opacity: 55 }, format: 'png', quality: 84, }); let recorded = null; const result = await renderImageVersion({ uploadRoot, sourcePath: 'verification/original.png', rootPath: 'verification/original.png', context: 'verification', operations: normalized, recordVersion: data => { recorded = data; return { id: 1, ...data }; }, }); const metadata = await sharp(join(uploadRoot, result.filename)).metadata(); if (metadata.format !== 'png') throw new Error(`Falsches Ausgabeformat: ${metadata.format}`); if (metadata.width > 640 || metadata.height > 640) throw new Error(`Zielgröße überschritten: ${metadata.width}x${metadata.height}`); if (!recorded || recorded.root_path !== 'verification/original.png') throw new Error('Versionenkette wurde nicht korrekt protokolliert.'); if (recorded.output_path !== result.filename || recorded.source_path !== 'verification/original.png') throw new Error('Quell-/Zielpfad der Version ist inkonsistent.'); if (!recorded.operations.crop || recorded.operations.sharpness !== 38) throw new Error('Bearbeitungsparameter wurden nicht vollständig gespeichert.'); if (!recorded.operations.remove_background) throw new Error('Hintergrundentfernung wurde nicht gespeichert.'); if (!recorded.operations.watermark?.enabled || recorded.operations.watermark.text !== 'Vendoo Test') throw new Error('Wasserzeichen wurde nicht gespeichert.'); const stats = await sharp(join(uploadRoot, result.filename)).stats(); const alpha = stats.channels[3]; if (!alpha || alpha.min !== 0 || alpha.max !== 255) throw new Error('Transparenter Hintergrund wurde nicht erzeugt.'); let traversalBlocked = false; try { resolveEditableImage(uploadRoot, '../outside.png'); } catch { traversalBlocked = true; } if (!traversalBlocked) throw new Error('Pfadschutz hat Traversal nicht blockiert.'); console.log(JSON.stringify({ ok: true, source: { width: 960, height: 720, format: 'png' }, output: { width: metadata.width, height: metadata.height, format: metadata.format, filename: result.filename }, operations: { undo_redo_state_shape: true, crop: recorded.operations.crop, rotation: recorded.operations.rotation, straighten: recorded.operations.straighten, tone_controls: ['exposure', 'brightness', 'contrast', 'highlights', 'shadows', 'temperature', 'saturation', 'sharpness'], resize: recorded.operations.resize, quality: recorded.operations.quality, remove_background: recorded.operations.remove_background, watermark: recorded.operations.watermark, }, non_destructive: true, version_recorded: true, traversal_blocked: true, }, null, 2)); } finally { rmSync(temp, { recursive: true, force: true }); }