"use client"; import { useState, useRef, ChangeEvent, useEffect } from "react"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Upload, Download, X, Trash2, Check } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { Switch } from "@/components/ui/switch"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { Slider } from "@/components/ui/slider"; import { ObjectPositionControl } from "./object-position-control"; const aspectRatios = [ { name: "Custom", value: "custom" }, { name: "1:1 (Square)", value: "1/1" }, { name: "4:3 (Standard)", value: "4/3" }, { name: "3:2 (Photography)", value: "3/2" }, { name: "16:9 (Widescreen)", value: "16/9" }, ]; export function ImageConverter() { const [images, setImages] = useState([]); const [previewUrls, setPreviewUrls] = useState([]); const [filenames, setFilenames] = useState([]); const [width, setWidth] = useState(""); const [height, setHeight] = useState(""); const [aspectRatio, setAspectRatio] = useState("custom"); const [format, setFormat] = useState<"png" | "jpeg" | "webp">("webp"); const [quality, setQuality] = useState(90); const [prefix, setPrefix] = useState(""); const [suffix, setSuffix] = useState(""); const [useCounter, setUseCounter] = useState(false); const [counterStart, setCounterStart] = useState(1); const [counterDigits, setCounterDigits] = useState(3); const [scaleMode, setScaleMode] = useState<'fill' | 'cover' | 'contain'>('cover'); const [objectPosition, setObjectPosition] = useState('center center'); const [isConverting, setIsConverting] = useState(false); const [convertingIndex, setConvertingIndex] = useState(null); const [isDraggingOver, setIsDraggingOver] = useState(false); const fileInputRef = useRef(null); useEffect(() => { return () => { previewUrls.forEach((url) => URL.revokeObjectURL(url)); }; }, [previewUrls]); const handleFiles = (files: FileList | null) => { if (!files || files.length === 0) return; const imageFiles = Array.from(files).filter((file) => file.type.startsWith("image/") ); if (imageFiles.length === 0) { toast.error("No valid image files found."); return; } const newImages = [...images, ...imageFiles]; const newPreviewUrls = [ ...previewUrls, ...imageFiles.map((file) => URL.createObjectURL(file)), ]; const newFilenames = [ ...filenames, ...imageFiles.map((file) => file.name.substring(0, file.name.lastIndexOf(".")) ), ]; setImages(newImages); setPreviewUrls(newPreviewUrls); setFilenames(newFilenames); toast.success(`${imageFiles.length} image(s) added.`); }; const handleImageChange = (e: ChangeEvent) => { handleFiles(e.target.files); if (e.target) e.target.value = ""; }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDraggingOver(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); setIsDraggingOver(false); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDraggingOver(false); handleFiles(e.dataTransfer.files); }; const handleRemoveImage = (indexToRemove: number) => { URL.revokeObjectURL(previewUrls[indexToRemove]); const newImages = images.filter((_, i) => i !== indexToRemove); const newPreviewUrls = previewUrls.filter((_, i) => i !== indexToRemove); const newFilenames = filenames.filter((_, i) => i !== indexToRemove); setImages(newImages); setPreviewUrls(newPreviewUrls); setFilenames(newFilenames); }; const handleClearAll = () => { previewUrls.forEach((url) => URL.revokeObjectURL(url)); setImages([]); setPreviewUrls([]); setFilenames([]); toast.info("All images cleared."); }; const handleFilenameChange = (index: number, newName: string) => { const newFilenames = [...filenames]; newFilenames[index] = newName; setFilenames(newFilenames); }; const generateFinalFilename = (index: number, withDimensions: boolean = false) => { const baseName = filenames[index] || "filename"; let finalName = `${prefix}${baseName}${suffix}`; if (useCounter) { const counter = (index + counterStart).toString().padStart(counterDigits, '0'); finalName += `${counter}`; } if (withDimensions && width && height) { finalName += `_${width}x${height}`; } return finalName; }; const convertAndDownload = (image: File, previewUrl: string, index: number) => { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = "anonymous"; img.src = previewUrl; img.onload = () => { const canvas = document.createElement("canvas"); const targetWidth = width ? Number(width) : img.naturalWidth; const targetHeight = height ? Number(height) : img.naturalHeight; canvas.width = targetWidth; canvas.height = targetHeight; const ctx = canvas.getContext("2d"); if (ctx) { const sWidth = img.naturalWidth; const sHeight = img.naturalHeight; const dWidth = targetWidth; const dHeight = targetHeight; if (scaleMode === 'fill' || !width || !height) { ctx.drawImage(img, 0, 0, dWidth, dHeight); } else { const sourceRatio = sWidth / sHeight; const targetRatio = dWidth / dHeight; let sx = 0, sy = 0, sRenderWidth = sWidth, sRenderHeight = sHeight; let dx = 0, dy = 0, dRenderWidth = dWidth, dRenderHeight = dHeight; const [hPos, vPos] = objectPosition.split(' '); if (scaleMode === 'cover') { if (sourceRatio > targetRatio) { sRenderHeight = sHeight; sRenderWidth = sHeight * targetRatio; if (hPos === 'center') sx = (sWidth - sRenderWidth) / 2; if (hPos === 'right') sx = sWidth - sRenderWidth; } else { sRenderWidth = sWidth; sRenderHeight = sWidth / targetRatio; if (vPos === 'center') sy = (sHeight - sRenderHeight) / 2; if (vPos === 'bottom') sy = sHeight - sRenderHeight; } ctx.drawImage(img, sx, sy, sRenderWidth, sRenderHeight, 0, 0, dWidth, dHeight); } else if (scaleMode === 'contain') { if (sourceRatio > targetRatio) { dRenderWidth = dWidth; dRenderHeight = dWidth / sourceRatio; if (vPos === 'center') dy = (dHeight - dRenderHeight) / 2; if (vPos === 'bottom') dy = dHeight - dRenderHeight; } else { dRenderHeight = dHeight; dRenderWidth = dHeight * sourceRatio; if (hPos === 'center') dx = (dWidth - dRenderWidth) / 2; if (hPos === 'right') dx = dWidth - dRenderWidth; } ctx.drawImage(img, 0, 0, sWidth, sHeight, dx, dy, dRenderWidth, dRenderHeight); } } const mimeType = `image/${format}`; const dataUrl = canvas.toDataURL(mimeType, format === 'png' ? undefined : quality / 100); const link = document.createElement("a"); link.href = dataUrl; const dimensionSuffix = width && height ? `_${width}x${height}` : ''; link.download = `${generateFinalFilename(index)}${dimensionSuffix}.${format}`; document.body.appendChild(link); link.click(); document.body.removeChild(link); resolve(); } else { reject(new Error(`Could not process ${image.name}.`)); } }; img.onerror = () => { reject(new Error(`Failed to load ${image.name} for conversion.`)); }; }); }; const handleConvertAndDownloadAll = async () => { if (images.length === 0) { toast.error("Please upload images first."); return; } setIsConverting(true); toast.info(`Starting conversion for ${images.length} images...`); const conversionPromises = images.map((image, index) => convertAndDownload(image, previewUrls[index], index) ); try { await Promise.all(conversionPromises); toast.success(`Successfully exported all ${images.length} images!`); } catch (error) { if (error instanceof Error) { toast.error(error.message); } else { toast.error("An unknown error occurred during conversion."); } } finally { setIsConverting(false); } }; const handleConvertAndDownloadSingle = async (index: number) => { setConvertingIndex(index); toast.info(`Starting conversion for ${filenames[index]}...`); try { await convertAndDownload(images[index], previewUrls[index], index); toast.success(`Successfully exported ${filenames[index]}!`); } catch (error) { if (error instanceof Error) { toast.error(error.message); } else { toast.error("An unknown error occurred during conversion."); } } finally { setConvertingIndex(null); } }; const handleApplySettings = () => { toast.info("Settings updated and will be used for all downloads."); }; const handleAspectRatioChange = (value: string) => { setAspectRatio(value); if (value === "custom") { return; } const [w, h] = value.split("/").map(Number); let newWidth: number; let newHeight: number; if (w > h) { newWidth = 1000; newHeight = Math.round((1000 * h) / w); } else if (h > w) { newHeight = 1000; newWidth = Math.round((1000 * w) / h); } else { newWidth = 1000; newHeight = 1000; } setWidth(newWidth); setHeight(newHeight); }; const handleWidthChange = (e: React.ChangeEvent) => { setWidth(e.target.value); setAspectRatio("custom"); }; const handleHeightChange = (e: React.ChangeEvent) => { setHeight(e.target.value); setAspectRatio("custom"); }; const hasImages = images.length > 0; return (

Image Settings

Adjust resolution and scaling for all images.

{scaleMode !== 'fill' && (
setObjectPosition(pos)} />
)}

Filename Settings

Customize the output filenames.

setPrefix(e.target.value)} />
setSuffix(e.target.value)} />
{useCounter && (
setCounterStart(Math.max(0, Number(e.target.value)))} min="0" />
setCounterDigits(Math.max(1, Number(e.target.value)))} min="1" />
)}

Quality Settings

Choose format and compression level.

{quality}%
setQuality(value[0])} disabled={format === 'png'} /> {format === 'png' && (

Quality slider is disabled for PNG (lossless format).

)}
fileInputRef.current?.click()} >

Click to upload or drag and drop

Select files from your computer

{hasImages && (
Uploaded Images
{previewUrls.map((url, index) => { const finalFilename = generateFinalFilename(index, true); return (
{`Preview
handleFilenameChange(index, e.target.value)} className="text-sm font-medium h-8 mt-1" />

Final name: {finalFilename}.{format}

); })}
)}
); }