import { Download, FileText, Moon, Mountain, Sparkles, User, UtensilsCrossed, Wand2, X, } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { ProgressCard } from "@/components/common/progress-card"; import { useTranslation } from "@/contexts/i18n-context"; import { useToolProcessor } from "@/hooks/use-tool-processor"; import { format } from "@/lib/format"; import { useFileStore } from "@/stores/file-store"; type EnhancementMode = "auto" | "portrait" | "landscape" | "low-light" | "food" | "document"; interface AnalysisScores { exposure: number; contrast: number; whiteBalance: number; saturation: number; sharpness: number; noise: number; } interface CorrectionParams { brightness: number; contrast: number; temperature: number; saturation: number; sharpness: number; denoise: number; } interface AnalysisData { scores: AnalysisScores; corrections: CorrectionParams; issues: string[]; suggestedMode: EnhancementMode; } const MODES: { value: EnhancementMode; label: string; icon: typeof Sparkles }[] = [ { value: "auto", label: "Auto", icon: Sparkles }, { value: "portrait", label: "Portrait", icon: User }, { value: "landscape", label: "Landscape", icon: Mountain }, { value: "low-light", label: "Low Light", icon: Moon }, { value: "food", label: "Food", icon: UtensilsCrossed }, { value: "document", label: "Document", icon: FileText }, ]; const PRESET_MULTIPLIERS: Record> = { auto: { brightness: 1.0, contrast: 1.0, temperature: 1.0, saturation: 1.0, sharpness: 1.0, denoise: 1.0, clahe: 1.0, normalise: 1.0, }, portrait: { brightness: 0.8, contrast: 0.7, temperature: 1.2, saturation: 0.6, sharpness: 0.5, denoise: 1.5, clahe: 0.7, normalise: 0.8, }, landscape: { brightness: 1.0, contrast: 1.3, temperature: 1.0, saturation: 1.4, sharpness: 1.5, denoise: 0.5, clahe: 1.3, normalise: 1.2, }, "low-light": { brightness: 1.8, contrast: 1.5, temperature: 1.0, saturation: 0.8, sharpness: 1.2, denoise: 2.0, clahe: 1.5, normalise: 1.5, }, food: { brightness: 0.8, contrast: 1.1, temperature: 1.3, saturation: 1.3, sharpness: 1.2, denoise: 0.5, clahe: 1.1, normalise: 1.0, }, document: { brightness: 1.5, contrast: 2.0, temperature: 1.0, saturation: 0.0, sharpness: 2.0, denoise: 2.0, clahe: 2.0, normalise: 1.5, }, }; const ISSUE_LABELS: Record = { underexposed: "Low Exposure", overexposed: "Overexposed", "low-contrast": "Flat Contrast", "color-cast": "Color Cast", desaturated: "Desaturated", "soft-focus": "Soft Focus", noisy: "Noisy", }; const ISSUE_TO_TOGGLE: Record = { underexposed: "exposure", overexposed: "exposure", "low-contrast": "contrast", "color-cast": "whiteBalance", desaturated: "saturation", "soft-focus": "sharpness", noisy: "denoise", }; interface ImageEnhancementControlsProps { settings?: Record; onChange?: (settings: Record) => void; onPreviewFilter?: (filter: string) => void; } export function ImageEnhancementControls({ settings: _initialSettings, onChange, onPreviewFilter, }: ImageEnhancementControlsProps) { const { t } = useTranslation(); const { files } = useFileStore(); const [mode, setMode] = useState("auto"); const [intensity, setIntensity] = useState(50); const [analysis, setAnalysis] = useState(null); const [analyzing, setAnalyzing] = useState(false); const [toggles, setToggles] = useState>({ exposure: true, contrast: true, whiteBalance: true, saturation: true, sharpness: true, denoise: true, }); const [deepEnhance, setDeepEnhance] = useState(false); const analyzeAbortRef = useRef(null); const onChangeRef = useRef(onChange); onChangeRef.current = onChange; // Analyze image when files change useEffect(() => { if (files.length === 0) { setAnalysis(null); return; } analyzeAbortRef.current?.abort(); const controller = new AbortController(); analyzeAbortRef.current = controller; setAnalyzing(true); const formData = new FormData(); formData.append("file", files[0]); fetch("/api/v1/tools/image-enhancement/analyze", { method: "POST", body: formData, signal: controller.signal, }) .then((res) => (res.ok ? res.json() : Promise.reject(new Error("Analysis failed")))) .then((data: AnalysisData) => { setAnalysis(data); if (data.suggestedMode !== "auto") { setMode(data.suggestedMode); } }) .catch((err) => { if (err.name !== "AbortError") { console.error("Analysis error:", err); } }) .finally(() => setAnalyzing(false)); return () => controller.abort(); }, [files]); // Emit settings when mode/intensity/toggles change useEffect(() => { onChangeRef.current?.({ mode, intensity, corrections: toggles, deepEnhance }); }, [mode, intensity, toggles, deepEnhance]); // CSS filter preview useEffect(() => { if (!onPreviewFilter || !analysis) { onPreviewFilter?.(""); return; } const presets = PRESET_MULTIPLIERS[mode]; const scale = intensity / 50; const c = analysis.corrections; const parts: string[] = []; if (toggles.exposure && Math.abs(c.brightness) > 2) { const adj = c.brightness * (presets.brightness ?? 1) * scale; parts.push(`brightness(${1 + adj / 100})`); } if (toggles.contrast && Math.abs(c.contrast) > 2) { const adj = c.contrast * (presets.contrast ?? 1) * scale; parts.push(`contrast(${1 + adj / 100})`); } if (toggles.saturation && Math.abs(c.saturation) > 2) { const adj = c.saturation * (presets.saturation ?? 1) * scale; parts.push(`saturate(${1 + adj / 100})`); } if (toggles.whiteBalance && Math.abs(c.temperature) > 2) { parts.push("url(#snapotter-enhance-temp-filter)"); } if (toggles.sharpness && c.sharpness > 2) { parts.push("url(#snapotter-enhance-sharpen-filter)"); } onPreviewFilter(parts.join(" ")); }, [analysis, mode, intensity, toggles, onPreviewFilter]); const toggleCorrection = (key: string) => { setToggles((prev) => ({ ...prev, [key]: !prev[key] })); }; const tempAdj = analysis ? (analysis.corrections.temperature * (PRESET_MULTIPLIERS[mode].temperature ?? 1) * intensity) / 50 / 100 : 0; const sharpAdj = analysis ? (analysis.corrections.sharpness * (PRESET_MULTIPLIERS[mode].sharpness ?? 1) * intensity) / 50 / 100 : 0; return ( <> {/* Hidden SVG filters for preview */} {toggles.whiteBalance && Math.abs(tempAdj) > 0.02 && ( White balance temperature filter )} {toggles.sharpness && sharpAdj > 0.02 && ( Sharpen filter )} {/* Mode selector */}

{t.toolSettings.imageEnhancement.enhancementMode}

{MODES.map(({ value, label, icon: Icon }) => ( ))}
{/* Intensity slider */}

{t.toolSettings.imageEnhancement.intensity}

{intensity}%
setIntensity(Number(e.target.value))} className="w-full mt-1" />
{/* Deep Enhance toggle */}

{t.toolSettings.imageEnhancement.deepEnhance}

Removes noise and artifacts using AI

{/* Analysis badges */} {analyzing && (
Analyzing image...
)} {analysis && !analyzing && (

{t.toolSettings.imageEnhancement.detectedIssues}

{analysis.issues.length === 0 ? (

Image looks good. Fine-tune with the intensity slider.

) : (
{analysis.issues.map((issue) => { const toggleKey = ISSUE_TO_TOGGLE[issue]; const isEnabled = toggleKey ? toggles[toggleKey] !== false : true; return ( ); })}
)} {/* Score indicators */}
{( [ ["Exposure", analysis.scores.exposure], ["Contrast", analysis.scores.contrast], ["White Bal", analysis.scores.whiteBalance], ["Saturation", analysis.scores.saturation], ["Sharpness", analysis.scores.sharpness], ["Noise", analysis.scores.noise], ] as const ).map(([label, score]) => (
65 ? "bg-blue-500" : "bg-emerald-500" }`} style={{ width: `${score}%` }} />
{label}
))}
)} ); } // Wrapper with process/download flow export function ImageEnhancementSettings({ onPreviewFilter, }: { onPreviewFilter?: (filter: string) => void; }) { const { files } = useFileStore(); const { processFiles, processAllFiles, processing, error, downloadUrl, originalSize, processedSize, progress, } = useToolProcessor("image-enhancement"); const [settings, setSettings] = useState>({}); const hasFile = files.length > 0; const handleProcess = () => { if (files.length > 1) { processAllFiles(files, settings); } else { processFiles(files, settings); } }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (hasFile && !processing) handleProcess(); }; return (
{error &&

{error}

} {originalSize != null && processedSize != null && (

Original: {(originalSize / 1024).toFixed(1)} KB

Enhanced: {(processedSize / 1024).toFixed(1)} KB

)} {processing ? ( 1 ? `Enhancing ${files.length} images` : "Enhancing image"} percent={progress.percent} elapsed={progress.elapsed} /> ) : ( )} {downloadUrl && files.length <= 1 && ( Download )} ); }