Density — image compressor & converter
● client-side photo lab
DENSITY
Drop in an image, dial down its density, and carry it out lighter — JPEG, PNG, WebP or AVIF.
0 images
f/2.8 — best
f/4 — high
f/5.6 — balanced
f/8 — standard
f/11 — aggressive
f/16 — max
WebP
JPEG
PNG
AVIF
Apply to all
Download all (.zip)
Clear
' +
'' +
'
' +
'
' +
'
' + escapeHtml(card.name) + '
' +
'
' +
'' + card.originalExt + ' ' +
'' + card.naturalWidth + '×' + card.naturalHeight + ' ' +
'' + formatBytes(card.originalSize) + ' ' +
'
' +
'
' +
'
' +
'
' +
'
Compression f/5.6 · 75% · Balanced
' +
'
' +
'
' + stopsHtml + '
' +
'
' +
'
' +
'
Convert to
' +
'
' + chipButtons + '
' +
'
' +
'
' +
'
' +
'
' +
' ';
el.querySelector('[data-action="remove"]').addEventListener('click', function(){ removeCard(card.id); });
var qSlider = el.querySelector('[data-role="quality"]');
qSlider.addEventListener('input', function(){
card.settings.qIndex = parseInt(qSlider.value, 10);
updateQualityLabel(card);
setChipsLoading(card);
clearTimeout(card.debounceTimer);
card.debounceTimer = setTimeout(function(){ recomputeCard(card); }, 300);
});
el.querySelectorAll('[data-role="chips"] .chip').forEach(function(btn){
btn.addEventListener('click', function(){
if (btn.disabled) return;
var fmt = btn.dataset.fmt;
card.settings.format = fmt;
updateChipActive(card);
updateResultPanel(card);
});
});
var resizeToggle = el.querySelector('[data-role="resize-toggle"]');
var resizeFields = el.querySelector('[data-role="resize-fields"]');
resizeToggle.addEventListener('change', function(){
card.settings.resize = resizeToggle.checked;
resizeFields.hidden = !resizeToggle.checked;
recomputeCard(card);
});
var resizeInput = el.querySelector('[data-role="resize-input"]');
resizeInput.addEventListener('input', function(){
var v = parseInt(resizeInput.value, 10);
if (isNaN(v)) return;
v = Math.max(50, Math.min(20000, v));
card.settings.maxDim = v;
clearTimeout(card.debounceTimer);
card.debounceTimer = setTimeout(function(){ recomputeCard(card); }, 400);
});
el.querySelector('[data-role="download"]').addEventListener('click', function(){
if (!card.downloadURL) return;
var fmt = card.settings.format;
triggerDownload(card.downloadURL, card.base + '.' + EXT[fmt]);
});
updateQualityLabel(card);
updateChipActive(card);
}
function updateQualityLabel(card){
var stop = QUALITY_STOPS[card.settings.qIndex];
var labelEl = card.el.querySelector('[data-role="qlabel"]');
if (labelEl) labelEl.textContent = stop.label + ' · ' + stop.q + '% · ' + stop.desc;
card.el.querySelectorAll('[data-role="stops"] span').forEach(function(s){
s.classList.toggle('active', parseInt(s.dataset.i, 10) === card.settings.qIndex);
});
}
function updateChipActive(card){
card.el.querySelectorAll('[data-role="chips"] .chip').forEach(function(btn){
btn.classList.toggle('active', btn.dataset.fmt === card.settings.format);
});
}
function setChipsLoading(card){
card.el.querySelectorAll('[data-role="chips"] .chip').forEach(function(btn){
if (!btn.disabled){
var sz = btn.querySelector('.sz');
if (sz) sz.textContent = '…';
var badge = btn.querySelector('.badge');
if (badge) badge.remove();
}
});
var outSize = card.el.querySelector('[data-role="outsize"]');
if (outSize) outSize.textContent = '…';
}
function recomputeCard(card){
if (card.errored) return;
card.busy = true;
setChipsLoading(card);
var dims = getTargetDims(card);
card.outW = dims.w; card.outH = dims.h;
var q = QUALITY_STOPS[card.settings.qIndex].q;
var jobs = ALL_FORMATS.filter(function(f){ return SUPPORTED[f]; }).map(function(fmt){
return computeBlob(card.img, fmt, q, dims.w, dims.h)
.then(function(blob){ return { fmt: fmt, blob: blob }; })
.catch(function(){ return { fmt: fmt, blob: null }; });
});
Promise.all(jobs).then(function(results){
var blobs = {};
results.forEach(function(r){ blobs[r.fmt] = r.blob; });
card.blobs = blobs;
card.busy = false;
updateCardUI(card);
updateAggregate();
});
}
function updateCardUI(card){
// update chips
var smallestFmt = null, smallestSize = Infinity;
Object.keys(card.blobs).forEach(function(fmt){
var blob = card.blobs[fmt];
if (blob && blob.size < smallestSize){ smallestSize = blob.size; smallestFmt = fmt; }
});
card.el.querySelectorAll('[data-role="chips"] .chip').forEach(function(btn){
var fmt = btn.dataset.fmt;
var blob = card.blobs[fmt];
var sz = btn.querySelector('.sz');
if (!SUPPORTED[fmt]){
if (sz) sz.textContent = 'unsupported';
return;
}
if (sz) sz.textContent = blob ? formatBytes(blob.size) : 'failed';
var existingBadge = btn.querySelector('.badge');
if (existingBadge) existingBadge.remove();
if (fmt === smallestFmt){
var badge = document.createElement('span');
badge.className = 'badge';
badge.textContent = 'smallest';
btn.appendChild(badge);
}
});
updateResultPanel(card);
}
function updateResultPanel(card){
var fmt = card.settings.format;
var blob = card.blobs ? card.blobs[fmt] : null;
var outEl = card.el.querySelector('[data-role="outsize"]');
var pctEl = card.el.querySelector('[data-role="pct"]');
var dlBtn = card.el.querySelector('[data-role="download"]');
if (card.downloadURL){
URL.revokeObjectURL(card.downloadURL);
card.downloadURL = null;
}
if (!blob){
if (outEl) outEl.textContent = SUPPORTED[fmt] ? '…' : 'unsupported';
if (pctEl) pctEl.textContent = '';
if (dlBtn) dlBtn.disabled = true;
return;
}
card.downloadURL = URL.createObjectURL(blob);
if (outEl) outEl.textContent = formatBytes(blob.size);
var pct = pctChange(card.originalSize, blob.size);
if (pctEl){
pctEl.textContent = (pct >= 0 ? '−' : '+') + Math.abs(pct).toFixed(0) + '%';
pctEl.className = 'pct-badge ' + (pct >= 0 ? 'down' : 'up');
}
if (dlBtn) dlBtn.disabled = false;
}
function removeCard(id){
var card = state.cards.get(id);
if (!card) return;
if (card.objectURL) URL.revokeObjectURL(card.objectURL);
if (card.downloadURL) URL.revokeObjectURL(card.downloadURL);
if (card.el && card.el.parentNode) card.el.parentNode.removeChild(card.el);
state.cards.delete(id);
state.order = state.order.filter(function(x){ return x !== id; });
updateAggregate();
if (state.cards.size === 0) hideGlobalControls();
}
// ---------- Aggregate / global controls ----------
function updateAggregate(){
var n = state.cards.size;
var totalOrig = 0, totalOut = 0, anyOut = false;
state.cards.forEach(function(card){
totalOrig += card.originalSize || 0;
var blob = card.blobs ? card.blobs[card.settings.format] : null;
if (blob){ totalOut += blob.size; anyOut = true; }
else { totalOut += card.originalSize || 0; }
});
var el = document.getElementById('totalsText');
if (!el) return;
if (n === 0){
el.textContent = '0 images';
return;
}
var text = n + (n === 1 ? ' image' : ' images') + ' · ' + formatBytes(totalOrig) + ' → ' + formatBytes(totalOut);
if (anyOut && totalOrig > 0){
var pct = pctChange(totalOrig, totalOut);
text += ' ·
' + (pct >= 0 ? '−' : '+') + Math.abs(pct).toFixed(0) + '% ';
}
el.innerHTML = text;
}
function showGlobalControls(){
document.getElementById('statsbar').classList.add('show');
}
function hideGlobalControls(){
document.getElementById('statsbar').classList.remove('show');
}
function applyToAll(){
var qIndex = parseInt(document.getElementById('globalQuality').value, 10);
var fmt = document.getElementById('globalFormat').value;
state.cards.forEach(function(card){
if (card.errored) return;
card.settings.qIndex = qIndex;
card.settings.format = fmt;
updateQualityLabel(card);
updateChipActive(card);
var qSlider = card.el.querySelector('[data-role="quality"]');
if (qSlider) qSlider.value = qIndex;
recomputeCard(card);
});
}
function clearAll(){
Array.from(state.order).forEach(function(id){ removeCard(id); });
}
function downloadAll(){
if (typeof JSZip === 'undefined'){
alert('The zip library failed to load (check your network connection).');
return;
}
var zip = new JSZip();
var used = {};
var any = false;
state.cards.forEach(function(card){
if (card.errored) return;
var fmt = card.settings.format;
var blob = card.blobs ? card.blobs[fmt] : null;
if (!blob) return;
var name = card.base + '.' + EXT[fmt];
if (used[name]){
used[name]++;
name = card.base + '-' + used[name] + '.' + EXT[fmt];
} else {
used[name] = 1;
}
zip.file(name, blob);
any = true;
});
if (!any){ alert('Nothing ready to download yet — wait for processing to finish.'); return; }
zip.generateAsync({ type: 'blob' }).then(function(content){
var url = URL.createObjectURL(content);
triggerDownload(url, 'Rajmedical-export.zip');
setTimeout(function(){ URL.revokeObjectURL(url); }, 4000);
});
}
// ---------- File intake ----------
function handleFiles(fileList){
var files = Array.from(fileList).filter(function(f){
return f.type.indexOf('image/') === 0 || /\.(heic|heif|avif)$/i.test(f.name);
});
files.forEach(addCard);
}
function wireDropzone(){
var dz = document.getElementById('dropzone');
var input = document.getElementById('fileInput');
dz.addEventListener('click', function(){ input.click(); });
dz.addEventListener('keydown', function(e){
if (e.key === 'Enter' || e.key === ' '){ e.preventDefault(); input.click(); }
});
input.addEventListener('change', function(){
handleFiles(input.files);
input.value = '';
});
['dragenter','dragover'].forEach(function(evt){
dz.addEventListener(evt, function(e){ e.preventDefault(); dz.classList.add('active'); });
});
['dragleave','drop'].forEach(function(evt){
dz.addEventListener(evt, function(e){ e.preventDefault(); dz.classList.remove('active'); });
});
dz.addEventListener('drop', function(e){
if (e.dataTransfer && e.dataTransfer.files) handleFiles(e.dataTransfer.files);
});
}
function init(){
Promise.all([
detectFormatSupport('image/webp'),
detectFormatSupport('image/avif')
]).then(function(res){
SUPPORTED.webp = res[0];
SUPPORTED.avif = res[1];
defaultFormat = SUPPORTED.webp ? 'webp' : 'jpeg';
document.getElementById('globalFormat').value = defaultFormat;
});
wireDropzone();
document.getElementById('applyAllBtn').addEventListener('click', applyToAll);
document.getElementById('downloadAllBtn').addEventListener('click', downloadAll);
document.getElementById('clearAllBtn').addEventListener('click', clearAll);
}
document.addEventListener('DOMContentLoaded', init);
})();