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 */}

PixelPress

{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
Original
Compressed
{compressedPreviewUrl ? ( Compressed ) : (
)}

Note: Processing happens entirely in your browser. No data is sent to any server.

)}
); }; export default ImageCompressor;