Image Tool
Image Compressor
Compress and convert images in your browser. No uploads, fully private — everything runs locally.
Drop image here or click to upload
JPEG · PNG · WebP · GIF / max 50 MB
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
(function () {
'use strict';
// ── STATE ────────────────────────────────────────────────────────
let state = {
file: null,
imgEl: null, // loaded HTMLImageElement (original)
origWidth: 0,
origHeight: 0,
origBytes: 0,
origDataURL: '',
format: 'jpeg', // 'jpeg' | 'png' | 'webp'
quality: 80,
maxWidth: null,
compBlob: null,
compObjectURL: null,
debounceTimer: null,
};
// ── ELEMENTS ─────────────────────────────────────────────────────
const uploadZone = document.getElementById('uploadZone');
const fileInput = document.getElementById('fileInput');
const toolPanel = document.getElementById('toolPanel');
const qualitySlider = document.getElementById('qualitySlider');
const qualityVal = document.getElementById('qualityVal');
const qualityField = document.getElementById('qualityField');
const maxWidthInput = document.getElementById('maxWidthInput');
const origImg = document.getElementById('origImg');
const origSizeEl = document.getElementById('origSize');
const compImg = document.getElementById('compImg');
const compPlaceholder = document.getElementById('compPlaceholder');
const compSizeEl = document.getElementById('compSize');
const savingsPct = document.getElementById('savingsPct');
const savingsCaption = document.getElementById('savingsCaption');
const sizeOrig = document.getElementById('sizeOrig');
const sizeNew = document.getElementById('sizeNew');
const processingInd = document.getElementById('processingIndicator');
const downloadBtn = document.getElementById('downloadBtn');
const downloadLabel = document.getElementById('downloadLabel');
const resetBtn = document.getElementById('resetBtn');
const newImageBtn = document.getElementById('newImageBtn');
const canvas = document.getElementById('workCanvas');
// ── UTILS ────────────────────────────────────────────────────────
function formatBytes(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
}
function getMimeType(fmt) {
return { jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp' }[fmt] || 'image/jpeg';
}
function getExtension(fmt) {
return { jpeg: 'jpg', png: 'png', webp: 'webp' }[fmt] || 'jpg';
}
function buildOutputName(origName, fmt) {
const dotIdx = origName.lastIndexOf('.');
const base = dotIdx > 0 ? origName.slice(0, dotIdx) : origName;
return base + '-compressed.' + getExtension(fmt);
}
function detectFormat(file) {
const t = file.type;
if (t === 'image/jpeg' || t === 'image/jpg') return 'jpeg';
if (t === 'image/png') return 'png';
if (t === 'image/webp') return 'webp';
return 'jpeg'; // gif → convert to jpeg
}
function setFormatActive(fmt) {
document.querySelectorAll('.fmt-btn').forEach(b => {
b.classList.toggle('active', b.dataset.fmt === fmt);
});
}
function updateQualityVisibility(fmt) {
// PNG is lossless — grey out slider but still show it
if (fmt === 'png') {
qualityField.style.opacity = '0.4';
qualityField.style.pointerEvents = 'none';
} else {
qualityField.style.opacity = '1';
qualityField.style.pointerEvents = '';
}
}
function revokeOldURL() {
if (state.compObjectURL) {
URL.revokeObjectURL(state.compObjectURL);
state.compObjectURL = null;
}
}
// ── FILE HANDLING ────────────────────────────────────────────────
function handleFile(file) {
if (!file || !file.type.startsWith('image/')) return;
if (file.size > 50 * 1024 * 1024) {
alert('File exceeds 50 MB limit.');
return;
}
state.file = file;
state.origBytes = file.size;
state.format = detectFormat(file);
state.quality = 80;
state.maxWidth = null;
// Reset UI controls
qualitySlider.value = 80;
qualityVal.textContent = '80%';
maxWidthInput.value = '';
setFormatActive(state.format);
updateQualityVisibility(state.format);
// Load image
const reader = new FileReader();
reader.onload = (e) => {
state.origDataURL = e.target.result;
const img = new Image();
img.onload = () => {
state.imgEl = img;
state.origWidth = img.naturalWidth;
state.origHeight = img.naturalHeight;
// Show original preview
origImg.src = state.origDataURL;
origSizeEl.textContent = formatBytes(state.origBytes);
// Show tool panel
uploadZone.classList.add('hidden');
toolPanel.classList.remove('hidden');
// Trigger first compression
compress();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// ── COMPRESSION ──────────────────────────────────────────────────
function compress() {
if (!state.imgEl) return;
// Show processing state
compImg.classList.add('hidden');
compPlaceholder.classList.remove('hidden');
compPlaceholder.innerHTML = '<span class="spinner"></span> processing…';
processingInd.classList.add('hidden');
downloadBtn.classList.add('hidden');
const fmt = state.format;
const quality = state.quality / 100;
const maxW = state.maxWidth;
let drawW = state.origWidth;
let drawH = state.origHeight;
if (maxW && maxW < drawW) {
drawH = Math.round((maxW / drawW) * drawH);
drawW = maxW;
}
canvas.width = drawW;
canvas.height = drawH;
const ctx = canvas.getContext('2d');
// For JPEG, fill with white background (no alpha channel)
if (fmt === 'jpeg') {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, drawW, drawH);
} else {
ctx.clearRect(0, 0, drawW, drawH);
}
ctx.drawImage(state.imgEl, 0, 0, drawW, drawH);
const mime = getMimeType(fmt);
const qualityArg = fmt === 'png' ? undefined : quality;
canvas.toBlob((blob) => {
if (!blob) {
compPlaceholder.innerHTML = '<span style="color:var(--error);font-size:11px;">Compression failed.</span>';
return;
}
revokeOldURL();
state.compBlob = blob;
state.compObjectURL = URL.createObjectURL(blob);
// Update compressed preview
compImg.src = state.compObjectURL;
compImg.onload = () => {
compPlaceholder.classList.add('hidden');
compImg.classList.remove('hidden');
};
// Update stats
const origB = state.origBytes;
const compB = blob.size;
const diff = origB - compB;
const pct = Math.round((diff / origB) * 100);
compSizeEl.textContent = formatBytes(compB);
sizeOrig.textContent = formatBytes(origB);
sizeNew.textContent = formatBytes(compB);
if (pct >= 5) {
savingsPct.textContent = '↓ ' + pct + '%';
savingsPct.className = 'savings-pct';
savingsCaption.textContent = 'smaller';
} else if (pct > 0) {
savingsPct.textContent = '↓ ' + pct + '%';
savingsPct.className = 'savings-pct minimal';
savingsCaption.textContent = 'smaller';
} else if (pct === 0) {
savingsPct.textContent = '0%';
savingsPct.className = 'savings-pct minimal';
savingsCaption.textContent = 'change';
} else {
savingsPct.textContent = '↑ ' + Math.abs(pct) + '%';
savingsPct.className = 'savings-pct negative';
savingsCaption.textContent = 'larger';
}
// Update download button
const outputName = buildOutputName(state.file.name, fmt);
downloadBtn.href = state.compObjectURL;
downloadBtn.download = outputName;
downloadLabel.textContent = 'Download ' + fmt.toUpperCase() + ' — ' + formatBytes(compB);
downloadBtn.classList.remove('hidden');
processingInd.classList.add('hidden');
}, mime, qualityArg);
}
// ── DEBOUNCED RECOMPRESS ─────────────────────────────────────────
function scheduleCompress() {
if (!state.imgEl) return;
processingInd.classList.remove('hidden');
downloadBtn.classList.add('hidden');
clearTimeout(state.debounceTimer);
state.debounceTimer = setTimeout(compress, 300);
}
// ── EVENT LISTENERS ──────────────────────────────────────────────
// Upload zone click/drag
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('drag-over');
});
uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag-over'));
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('drag-over');
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
});
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) handleFile(file);
fileInput.value = ''; // reset so same file can be re-selected
});
// Quality slider
qualitySlider.addEventListener('input', (e) => {
state.quality = parseInt(e.target.value);
qualityVal.textContent = state.quality + '%';
scheduleCompress();
});
// Format buttons
document.querySelectorAll('.fmt-btn').forEach(btn => {
btn.addEventListener('click', () => {
state.format = btn.dataset.fmt;
setFormatActive(state.format);
updateQualityVisibility(state.format);
scheduleCompress();
});
});
// Max width input
maxWidthInput.addEventListener('input', (e) => {
const v = parseInt(e.target.value);
state.maxWidth = (!isNaN(v) && v > 0) ? v : null;
scheduleCompress();
});
// Reset
resetBtn.addEventListener('click', () => {
if (!state.file) return;
qualitySlider.value = 80;
state.quality = 80;
qualityVal.textContent = '80%';
maxWidthInput.value = '';
state.maxWidth = null;
const defaultFmt = detectFormat(state.file);
state.format = defaultFmt;
setFormatActive(defaultFmt);
updateQualityVisibility(defaultFmt);
scheduleCompress();
});
// New image
newImageBtn.addEventListener('click', () => {
revokeOldURL();
state.file = null;
state.imgEl = null;
state.compBlob = null;
origImg.src = '';
compImg.src = '';
compImg.classList.add('hidden');
compPlaceholder.classList.remove('hidden');
compPlaceholder.innerHTML = '<span class="spinner"></span> processing…';
downloadBtn.classList.add('hidden');
toolPanel.classList.add('hidden');
uploadZone.classList.remove('hidden');
fileInput.value = '';
});
// Global paste support
document.addEventListener('paste', (e) => {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
for (const item of items) {
if (item.type.startsWith('image/')) {
handleFile(item.getAsFile());
break;
}
}
});
})();