import React, { useState, useEffect, useRef } from 'react';
import { Upload, Download, Settings, Image as ImageIcon, X, RefreshCw, ArrowRight, FileImage } from 'lucide-react';
const ImageCompressor = () => {
const [originalFile, setOriginalFile] = useState(null);
const [compressedFile, setCompressedFile] = useState(null);
const [isCompressing, setIsCompressing] = useState(false);
const [previewUrl, setPreviewUrl] = useState(null);
const [compressedPreviewUrl, setCompressedPreviewUrl] = useState(null);
// Compression Settings
const [quality, setQuality] = useState(0.8);
const [scale, setScale] = useState(1);
const [format, setFormat] = useState('image/jpeg');
const fileInputRef = useRef(null);
// Helper: Format bytes to human readable string
const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
// Handle File Selection
const handleFileChange = (event) => {
const file = event.target.files[0];
if (file && file.type.startsWith('image/')) {
processFile(file);
}
};
const processFile = (file) => {
setOriginalFile(file);
const objectUrl = URL.createObjectURL(file);
setPreviewUrl(objectUrl);
// Default output format to input format (unless it's not supported by canvas export, then jpeg)
if (file.type === 'image/png' || file.type === 'image/webp') {
setFormat(file.type);
} else {
setFormat('image/jpeg');
}
};
// Trigger compression when settings or file changes
useEffect(() => {
if (originalFile) {
compressImage(originalFile);
}
}, [originalFile, quality, scale, format]);
const compressImage = (file) => {
setIsCompressing(true);
const img = new Image();
img.src = URL.createObjectURL(file);
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const width = img.width * scale;
const height = img.height * scale;
canvas.width = width;
canvas.height = height;
// Draw image on canvas
// Fill white background if converting PNG to JPEG to avoid black background for transparency
if (format === 'image/jpeg') {
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(
(blob) => {
if (blob) {
setCompressedFile(blob);
const compressedUrl = URL.createObjectURL(blob);
setCompressedPreviewUrl(compressedUrl);
}
setIsCompressing(false);
},
format,
quality
);
};
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) {
processFile(file);
}
};
const resetApp = () => {
setOriginalFile(null);
setCompressedFile(null);
setPreviewUrl(null);
setCompressedPreviewUrl(null);
setQuality(0.8);
setScale(1);
};
const calculateSavings = () => {
if (!originalFile || !compressedFile) return 0;
const saved = originalFile.size - compressedFile.size;
const percentage = (saved / originalFile.size) * 100;
return Math.max(0, percentage).toFixed(1); // Ensure doesn't show negative if size increases (rare but possible with settings)
};
const downloadImage = () => {
if (!compressedFile) return;
const link = document.createElement('a');
link.href = compressedPreviewUrl;
// Generate filename
const originalName = originalFile.name.substring(0, originalFile.name.lastIndexOf('.')) || originalFile.name;
const ext = format.split('/')[1];
link.download = `${originalName}-compressed.${ext}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
{/* Header */}
{originalFile && (
)}
{!originalFile ? (
/* Empty State / Dropzone */
{ e.preventDefault(); e.stopPropagation(); }}
onDrop={handleDrop}
onClick={() => fileInputRef.current.click()}
>
Upload an image to compress
Drag and drop generic JPG, PNG, or WebP files here
) : (
/* Editor UI */
{/* Sidebar Controls */}
Compression Settings
{/* Quality Slider */}
{Math.round(quality * 100)}%
setQuality(parseFloat(e.target.value))}
className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
Low
High
{/* Scale Slider */}
{Math.round(scale * 100)}%
setScale(parseFloat(e.target.value))}
className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
Reduces dimensions to save more space.
{/* Format Selection */}
{['image/jpeg', 'image/png', 'image/webp'].map((fmt) => (
))}
{/* Stats Card */}
Results
Original Size:
{formatBytes(originalFile.size)}
Compressed:
originalFile.size ? 'text-red-600' : 'text-green-600'}`}>
{compressedFile ? formatBytes(compressedFile.size) : '...'}
Saved:
{calculateSavings()}%
{/* Preview Area */}
Preview
{isCompressing && Processing...}
{/* Comparison View - Simple Side by Side for MVP */}
Original
Compressed
{compressedPreviewUrl ? (

) : (
)}
Note: Processing happens entirely in your browser. No data is sent to any server.
)}
);
};
export default ImageCompressor;