Created
October 19, 2025 15:56
-
-
Save iamwrm/87d24c7b69c742b16002e27e8b3aec14 to your computer and use it in GitHub Desktop.
image compression v2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Smart Image Compressor</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 20px; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| overflow: hidden; | |
| } | |
| .header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 30px; | |
| text-align: center; | |
| } | |
| .header h1 { | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| } | |
| .header p { | |
| font-size: 1.1em; | |
| opacity: 0.9; | |
| } | |
| .content { | |
| padding: 30px; | |
| } | |
| .upload-section { | |
| border: 3px dashed #667eea; | |
| border-radius: 15px; | |
| padding: 40px; | |
| text-align: center; | |
| background: #f8f9ff; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| margin-bottom: 30px; | |
| } | |
| .upload-section:hover { | |
| background: #f0f2ff; | |
| border-color: #764ba2; | |
| } | |
| .upload-section.drag-over { | |
| background: #e8ebff; | |
| border-color: #764ba2; | |
| transform: scale(1.02); | |
| } | |
| .upload-icon { | |
| font-size: 4em; | |
| margin-bottom: 15px; | |
| } | |
| .controls { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| } | |
| .control-group { | |
| background: #f8f9ff; | |
| padding: 20px; | |
| border-radius: 10px; | |
| } | |
| .control-group label { | |
| display: block; | |
| font-weight: 600; | |
| margin-bottom: 10px; | |
| color: #333; | |
| } | |
| .control-group select, | |
| .control-group input[type="range"] { | |
| width: 100%; | |
| padding: 10px; | |
| border: 2px solid #ddd; | |
| border-radius: 8px; | |
| font-size: 1em; | |
| } | |
| .control-group select { | |
| cursor: pointer; | |
| background: white; | |
| } | |
| .quality-value { | |
| display: inline-block; | |
| margin-left: 10px; | |
| font-weight: bold; | |
| color: #667eea; | |
| } | |
| .preview-section { | |
| display: none; | |
| margin-top: 30px; | |
| } | |
| .preview-section.active { | |
| display: block; | |
| } | |
| .images-container { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| margin-bottom: 20px; | |
| } | |
| .image-preview { | |
| background: #f8f9ff; | |
| border-radius: 10px; | |
| padding: 20px; | |
| text-align: center; | |
| } | |
| .image-preview h3 { | |
| margin-bottom: 15px; | |
| color: #333; | |
| } | |
| .image-preview img { | |
| max-width: 100%; | |
| max-height: 400px; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
| } | |
| .image-preview .info { | |
| margin-top: 10px; | |
| font-size: 0.9em; | |
| color: #666; | |
| } | |
| .stats { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 25px; | |
| border-radius: 15px; | |
| margin-bottom: 20px; | |
| } | |
| .stats h3 { | |
| margin-bottom: 15px; | |
| font-size: 1.5em; | |
| } | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| } | |
| .stat-item { | |
| background: rgba(255, 255, 255, 0.2); | |
| padding: 15px; | |
| border-radius: 10px; | |
| backdrop-filter: blur(10px); | |
| } | |
| .stat-label { | |
| font-size: 0.9em; | |
| opacity: 0.9; | |
| margin-bottom: 5px; | |
| } | |
| .stat-value { | |
| font-size: 1.5em; | |
| font-weight: bold; | |
| } | |
| .detection-info { | |
| background: #f8f9ff; | |
| border-left: 4px solid #667eea; | |
| padding: 15px 20px; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| } | |
| .detection-info strong { | |
| color: #667eea; | |
| } | |
| .button { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| padding: 15px 40px; | |
| font-size: 1.1em; | |
| font-weight: 600; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| display: inline-block; | |
| text-decoration: none; | |
| } | |
| .button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4); | |
| } | |
| .button:active { | |
| transform: translateY(0); | |
| } | |
| .button-secondary { | |
| background: #6c757d; | |
| margin-left: 10px; | |
| } | |
| .button-container { | |
| text-align: center; | |
| margin-top: 20px; | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| @media (max-width: 768px) { | |
| .images-container { | |
| grid-template-columns: 1fr; | |
| } | |
| .header h1 { | |
| font-size: 1.8em; | |
| } | |
| .controls { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .loader { | |
| display: none; | |
| text-align: center; | |
| padding: 20px; | |
| } | |
| .loader.active { | |
| display: block; | |
| } | |
| .spinner { | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid #667eea; | |
| border-radius: 50%; | |
| width: 50px; | |
| height: 50px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 15px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🖼️ Smart Image Compressor</h1> | |
| <p>Auto-detects text vs photos and applies optimal compression</p> | |
| </div> | |
| <div class="content"> | |
| <div class="upload-section" id="uploadSection"> | |
| <div class="upload-icon">📁</div> | |
| <h2>Choose an image or drag & drop</h2> | |
| <p style="margin-top: 10px; color: #666;">Supports PNG, JPEG, WebP, BMP, GIF</p> | |
| <input type="file" id="fileInput" accept="image/*" style="display: none;"> | |
| </div> | |
| <div class="controls"> | |
| <div class="control-group"> | |
| <label>Output Format</label> | |
| <select id="formatSelect"> | |
| <option value="webp">WebP (Recommended)</option> | |
| <option value="png">PNG (Lossless)</option> | |
| <option value="jpeg">JPEG (Photos)</option> | |
| </select> | |
| </div> | |
| <div class="control-group"> | |
| <label>Compression Mode</label> | |
| <select id="modeSelect"> | |
| <option value="auto">Auto-detect</option> | |
| <option value="text">Text/Diagram</option> | |
| <option value="photo">Photo</option> | |
| </select> | |
| </div> | |
| <div class="control-group"> | |
| <label>Quality (for lossy formats) | |
| <span class="quality-value" id="qualityValue">85</span> | |
| </label> | |
| <input type="range" id="qualitySlider" min="1" max="100" value="85"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Color Levels (for text mode) | |
| <span class="quality-value" id="colorLevelsValue">32</span> | |
| </label> | |
| <input type="range" id="colorLevelsSlider" min="8" max="256" value="32" step="8"> | |
| </div> | |
| </div> | |
| <div class="loader" id="loader"> | |
| <div class="spinner"></div> | |
| <p>Compressing image...</p> | |
| </div> | |
| <div class="preview-section" id="previewSection"> | |
| <div class="detection-info" id="detectionInfo"></div> | |
| <div class="stats" id="stats"></div> | |
| <div class="images-container"> | |
| <div class="image-preview"> | |
| <h3>Original</h3> | |
| <img id="originalImage" alt="Original"> | |
| <div class="info" id="originalInfo"></div> | |
| </div> | |
| <div class="image-preview"> | |
| <h3>Compressed</h3> | |
| <img id="compressedImage" alt="Compressed"> | |
| <div class="info" id="compressedInfo"></div> | |
| </div> | |
| </div> | |
| <div class="button-container"> | |
| <button class="button" id="downloadButton">⬇️ Download Compressed Image</button> | |
| <button class="button button-secondary" id="resetButton">🔄 Compress Another</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <canvas id="canvas" style="display: none;"></canvas> | |
| <script> | |
| // DOM Elements | |
| const uploadSection = document.getElementById('uploadSection'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const formatSelect = document.getElementById('formatSelect'); | |
| const modeSelect = document.getElementById('modeSelect'); | |
| const qualitySlider = document.getElementById('qualitySlider'); | |
| const qualityValue = document.getElementById('qualityValue'); | |
| const colorLevelsSlider = document.getElementById('colorLevelsSlider'); | |
| const colorLevelsValue = document.getElementById('colorLevelsValue'); | |
| const loader = document.getElementById('loader'); | |
| const previewSection = document.getElementById('previewSection'); | |
| const detectionInfo = document.getElementById('detectionInfo'); | |
| const stats = document.getElementById('stats'); | |
| const originalImage = document.getElementById('originalImage'); | |
| const compressedImage = document.getElementById('compressedImage'); | |
| const originalInfo = document.getElementById('originalInfo'); | |
| const compressedInfo = document.getElementById('compressedInfo'); | |
| const downloadButton = document.getElementById('downloadButton'); | |
| const resetButton = document.getElementById('resetButton'); | |
| const canvas = document.getElementById('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let currentFile = null; | |
| let compressedBlob = null; | |
| // Event Listeners | |
| uploadSection.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', handleFileSelect); | |
| qualitySlider.addEventListener('input', (e) => { | |
| qualityValue.textContent = e.target.value; | |
| }); | |
| colorLevelsSlider.addEventListener('input', (e) => { | |
| colorLevelsValue.textContent = e.target.value; | |
| }); | |
| downloadButton.addEventListener('click', downloadCompressed); | |
| resetButton.addEventListener('click', reset); | |
| // Drag and Drop | |
| uploadSection.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadSection.classList.add('drag-over'); | |
| }); | |
| uploadSection.addEventListener('dragleave', () => { | |
| uploadSection.classList.remove('drag-over'); | |
| }); | |
| uploadSection.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadSection.classList.remove('drag-over'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| handleFile(files[0]); | |
| } | |
| }); | |
| function handleFileSelect(e) { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| handleFile(file); | |
| } | |
| } | |
| async function handleFile(file) { | |
| if (!file.type.startsWith('image/')) { | |
| alert('Please select an image file'); | |
| return; | |
| } | |
| currentFile = file; | |
| loader.classList.add('active'); | |
| previewSection.classList.remove('active'); | |
| // Load image | |
| const img = new Image(); | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| img.src = e.target.result; | |
| }; | |
| img.onload = () => { | |
| compressImage(img, file); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| function detectImageType(imageData) { | |
| const data = imageData.data; | |
| const colorMap = new Map(); | |
| const totalPixels = data.length / 4; | |
| // Sample pixels (for performance, check every 10th pixel) | |
| for (let i = 0; i < data.length; i += 40) { | |
| const r = data[i]; | |
| const g = data[i + 1]; | |
| const b = data[i + 2]; | |
| const color = `${r},${g},${b}`; | |
| colorMap.set(color, (colorMap.get(color) || 0) + 1); | |
| } | |
| const uniqueColors = colorMap.size; | |
| const sampledPixels = totalPixels / 10; | |
| const colorRatio = uniqueColors / sampledPixels; | |
| // Heuristic: text/diagrams have fewer colors | |
| if (uniqueColors < 50 || colorRatio < 0.1) { | |
| return 'text'; | |
| } | |
| return 'photo'; | |
| } | |
| function quantizeColors(imageData, levels = 32) { | |
| const data = imageData.data; | |
| const step = 256 / levels; | |
| // Reduce color depth by quantizing each channel | |
| for (let i = 0; i < data.length; i += 4) { | |
| // Quantize red channel | |
| data[i] = Math.round(data[i] / step) * step; | |
| // Quantize green channel | |
| data[i + 1] = Math.round(data[i + 1] / step) * step; | |
| // Quantize blue channel | |
| data[i + 2] = Math.round(data[i + 2] / step) * step; | |
| // Keep alpha channel unchanged | |
| } | |
| return imageData; | |
| } | |
| async function compressImage(img, file) { | |
| const format = formatSelect.value; | |
| const mode = modeSelect.value; | |
| const quality = parseInt(qualitySlider.value) / 100; | |
| const colorLevels = parseInt(colorLevelsSlider.value); | |
| // Set canvas size | |
| canvas.width = img.width; | |
| canvas.height = img.height; | |
| // Draw image | |
| ctx.drawImage(img, 0, 0); | |
| // Get image data for detection | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| // Detect image type | |
| let detectedType = mode === 'auto' ? detectImageType(imageData) : mode; | |
| // Show detection info | |
| const typeEmoji = detectedType === 'text' ? '📄' : '📸'; | |
| const typeName = detectedType === 'text' ? 'Text/Diagram' : 'Photo'; | |
| const strategy = detectedType === 'text' | |
| ? `Color quantization (${colorLevels} levels - preserves colors with sharp edges)` | |
| : `Quality-based compression (Q=${Math.round(quality * 100)})`; | |
| detectionInfo.innerHTML = ` | |
| <strong>${typeEmoji} Detected: ${typeName}</strong><br> | |
| Strategy: ${strategy} | |
| `; | |
| // Apply compression strategy | |
| if (detectedType === 'text') { | |
| const quantizedImageData = quantizeColors(imageData, colorLevels); | |
| ctx.putImageData(quantizedImageData, 0, 0); | |
| } | |
| // Convert to blob | |
| const mimeType = format === 'jpeg' ? 'image/jpeg' : `image/${format}`; | |
| const compressionQuality = detectedType === 'text' && format === 'jpeg' ? 0.95 : quality; | |
| canvas.toBlob((blob) => { | |
| compressedBlob = blob; | |
| // Display results | |
| displayResults(img, file, blob, detectedType); | |
| loader.classList.remove('active'); | |
| previewSection.classList.add('active'); | |
| }, mimeType, compressionQuality); | |
| } | |
| function displayResults(img, originalFile, compressedBlob, detectedType) { | |
| // Display original image | |
| originalImage.src = URL.createObjectURL(originalFile); | |
| originalInfo.innerHTML = ` | |
| ${img.width}×${img.height}px<br> | |
| ${formatBytes(originalFile.size)} | |
| `; | |
| // Display compressed image | |
| compressedImage.src = URL.createObjectURL(compressedBlob); | |
| compressedInfo.innerHTML = ` | |
| ${img.width}×${img.height}px<br> | |
| ${formatBytes(compressedBlob.size)} | |
| `; | |
| // Calculate stats | |
| const originalSize = originalFile.size; | |
| const compressedSize = compressedBlob.size; | |
| const savedBytes = originalSize - compressedSize; | |
| const reduction = ((savedBytes / originalSize) * 100).toFixed(1); | |
| // Display stats | |
| stats.innerHTML = ` | |
| <h3>✅ Compression Complete!</h3> | |
| <div class="stats-grid"> | |
| <div class="stat-item"> | |
| <div class="stat-label">Original Size</div> | |
| <div class="stat-value">${formatBytes(originalSize)}</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-label">Compressed Size</div> | |
| <div class="stat-value">${formatBytes(compressedSize)}</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-label">Space Saved</div> | |
| <div class="stat-value">${formatBytes(savedBytes)}</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-label">Reduction</div> | |
| <div class="stat-value">${reduction}%</div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function downloadCompressed() { | |
| if (!compressedBlob) return; | |
| const format = formatSelect.value; | |
| const extension = format === 'jpeg' ? 'jpg' : format; | |
| const filename = `compressed_${Date.now()}.${extension}`; | |
| const url = URL.createObjectURL(compressedBlob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| function reset() { | |
| currentFile = null; | |
| compressedBlob = null; | |
| fileInput.value = ''; | |
| previewSection.classList.remove('active'); | |
| } | |
| function formatBytes(bytes) { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment