Skip to content

Instantly share code, notes, and snippets.

@iamwrm
Created October 19, 2025 16:02
Show Gist options
  • Select an option

  • Save iamwrm/4cc3efa26789ebe10782a14cad62c94f to your computer and use it in GitHub Desktop.

Select an option

Save iamwrm/4cc3efa26789ebe10782a14cad62c94f to your computer and use it in GitHub Desktop.

Revisions

  1. iamwrm created this gist Oct 19, 2025.
    797 changes: 797 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,797 @@
    <!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); }
    }

    .toast {
    position: fixed;
    bottom: 30px;
    right: 30px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 15px 25px;
    border-radius: 10px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
    display: none;
    align-items: center;
    gap: 10px;
    z-index: 1000;
    animation: slideIn 0.3s ease;
    }

    .toast.show {
    display: flex;
    }

    @keyframes slideIn {
    from {
    transform: translateX(400px);
    opacity: 0;
    }
    to {
    transform: translateX(0);
    opacity: 1;
    }
    }

    @keyframes slideOut {
    from {
    transform: translateX(0);
    opacity: 1;
    }
    to {
    transform: translateX(400px);
    opacity: 0;
    }
    }

    .toast.hiding {
    animation: slideOut 0.3s ease;
    }

    .keyboard-hint {
    position: fixed;
    top: 20px;
    right: 20px;
    background: rgba(255, 255, 255, 0.95);
    padding: 10px 15px;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    font-size: 0.9em;
    color: #667eea;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 8px;
    z-index: 999;
    }

    .keyboard-hint kbd {
    background: #667eea;
    color: white;
    padding: 3px 8px;
    border-radius: 4px;
    font-family: monospace;
    font-size: 0.85em;
    }

    @media (max-width: 768px) {
    .keyboard-hint {
    display: none;
    }
    }
    </style>
    </head>
    <body>
    <div class="keyboard-hint">
    📋 Paste images: <kbd>Ctrl</kbd>+<kbd>V</kbd> or <kbd></kbd>+<kbd>V</kbd>
    </div>

    <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>
    <p style="margin-top: 10px; color: #667eea; font-weight: 600;">📋 Or press Ctrl+V / Cmd+V to paste from clipboard</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>

    <div class="toast" id="toast">
    <span id="toastIcon"></span>
    <span id="toastMessage"></span>
    </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');
    const toast = document.getElementById('toast');
    const toastIcon = document.getElementById('toastIcon');
    const toastMessage = document.getElementById('toastMessage');

    let currentFile = null;
    let compressedBlob = null;

    // Toast notification function
    function showToast(message, icon = '✓') {
    toastIcon.textContent = icon;
    toastMessage.textContent = message;
    toast.classList.remove('hiding');
    toast.classList.add('show');

    setTimeout(() => {
    toast.classList.add('hiding');
    setTimeout(() => {
    toast.classList.remove('show', 'hiding');
    }, 300);
    }, 3000);
    }

    // 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]);
    }
    });

    // Paste from clipboard
    document.addEventListener('paste', (e) => {
    // Don't paste if already processing
    if (loader.classList.contains('active')) {
    return;
    }

    const items = e.clipboardData?.items;
    if (!items) return;

    for (let i = 0; i < items.length; i++) {
    const item = items[i];

    // Check if item is an image
    if (item.type.startsWith('image/')) {
    e.preventDefault();
    const blob = item.getAsFile();
    if (blob) {
    handleFile(blob);
    showToast('Image pasted from clipboard!', '📋');
    // Flash the upload section to show paste worked
    uploadSection.style.background = '#e8ebff';
    setTimeout(() => {
    uploadSection.style.background = '#f8f9ff';
    }, 300);
    }
    break;
    }
    }
    });

    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);
    };

    img.onerror = () => {
    loader.classList.remove('active');
    showToast('Failed to load image', '❌');
    };

    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);

    // Show filename or 'Pasted Image' for clipboard images
    const fileName = originalFile.name || 'Pasted Image';

    originalInfo.innerHTML = `
    <strong style="display: block; margin-bottom: 5px;">${fileName}</strong>
    ${img.width}×${img.height}px<br>
    ${formatBytes(originalFile.size)}
    `;

    // Display compressed image
    compressedImage.src = URL.createObjectURL(compressedBlob);
    const compressedFormat = formatSelect.value.toUpperCase();
    compressedInfo.innerHTML = `
    <strong style="display: block; margin-bottom: 5px;">${compressedFormat} Format</strong>
    ${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;

    // Generate filename based on original file or timestamp for pasted images
    let filename;
    if (currentFile && currentFile.name) {
    const originalName = currentFile.name.replace(/\.[^/.]+$/, '');
    filename = `${originalName}_compressed.${extension}`;
    } else {
    filename = `pasted_image_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);

    showToast('Download started!', '⬇️');
    }

    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];
    }

    // Show welcome message on page load
    window.addEventListener('load', () => {
    setTimeout(() => {
    showToast('Ready! Click, drag, or paste (Ctrl+V) to start', '👋');
    }, 500);
    });
    </script>
    </body>
    </html>