Created
October 19, 2025 15:53
-
-
Save iamwrm/94209b8ce02cca8fcf0d155dc5efe78b to your computer and use it in GitHub Desktop.
image compression v1
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> | |
| <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 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; | |
| }); | |
| 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 convertTo1Bit(imageData) { | |
| const data = imageData.data; | |
| // Calculate average brightness | |
| let totalBrightness = 0; | |
| for (let i = 0; i < data.length; i += 4) { | |
| const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3; | |
| totalBrightness += brightness; | |
| } | |
| const threshold = totalBrightness / (data.length / 4); | |
| // Convert to black and white | |
| for (let i = 0; i < data.length; i += 4) { | |
| const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3; | |
| const value = brightness > threshold ? 255 : 0; | |
| data[i] = value; | |
| data[i + 1] = value; | |
| data[i + 2] = value; | |
| } | |
| return imageData; | |
| } | |
| async function compressImage(img, file) { | |
| const format = formatSelect.value; | |
| const mode = modeSelect.value; | |
| const quality = parseInt(qualitySlider.value) / 100; | |
| // 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' | |
| ? '1-bit black & white conversion' | |
| : `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 bwImageData = convertTo1Bit(imageData); | |
| ctx.putImageData(bwImageData, 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