AI Image Compressor — Centered
AI Image Compressor — Centered
Tool placed in the center of the page. Works offline, client-side. Drag & drop or choose files.
Notes: The tool is centered in the viewport — if you want it embedded inside an article middle (inline), tell me and I’ll supply that variant.

`;
listEl.appendChild(row);
});
}
function removeFile(i){
URL.revokeObjectURL(files[i].preview);
files.splice(i,1);
render();
}
async function compressOne(i){
const item = files[i];
const quality = parseFloat(qRange.value);
const minQuality = parseFloat(minQRange.value);
const maxW = parseInt(maxWidthInput.value) || 1600;
const fmt = formatSel.value;
const targetKB = parseInt(targetKBInput.value) || null;
const targetBytes = targetKB ? targetKB*1024 : null;
const watermark = watermarkInput.value.trim();
const wmPos = watermarkPos.value;
const img = await loadImage(item.file);
let w = img.width, h = img.height;
if (w > maxW) {
h = Math.round(maxW * (img.height / img.width));
w = maxW;
}
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img,0,0,w,h);
if (watermark) drawWatermark(ctx, watermark, w, h, wmPos);
// determine mime
let mime = 'image/webp';
if (fmt === 'jpeg') mime = 'image/jpeg';
if (fmt === 'png') mime = 'image/png';
if (fmt === 'auto') {
try { canvas.toDataURL('image/webp'); mime='image/webp'; } catch { mime='image/jpeg'; }
}
// helper
const canvasToBlob = (c,m,q)=> new Promise(res=>c.toBlob(res,m,q));
if (!targetBytes) {
const blob = await canvasToBlob(canvas,mime,quality);
item.compressed = blob;
render();
return;
}
// target mode: iterate
let q = quality;
let best = null;
while (q >= minQuality) {
const b = await canvasToBlob(canvas,mime,q);
if (!best || b.size < best.size) best = b;
if (b.size <= targetBytes) { item.compressed = b; render(); return; }
q = Math.round((q - 0.05)*100)/100;
}
item.compressed = best;
render();
}function drawWatermark(ctx, text, w, h, pos){
const fontSize = Math.max(12, Math.round(w/36));
ctx.font = `${fontSize}px sans-serif`;
ctx.textBaseline = 'bottom';
ctx.fillStyle = 'rgba(255,255,255,0.85)';
ctx.strokeStyle = 'rgba(0,0,0,0.55)';
ctx.lineWidth = 2;
const metrics = ctx.measureText(text);
const textW = metrics.width;
const pad = 12;
let x = w - textW - pad, y = h - pad;
if (pos === 'top-left') { x = pad; y = fontSize + pad; }
if (pos === 'top-right') { x = w - textW - pad; y = fontSize + pad; }
if (pos === 'bottom-left') { x = pad; y = h - pad; }
ctx.strokeText(text, x, y);
ctx.fillText(text, x, y);
}function loadImage(file){ return new Promise(res => { const img = new Image(); img.onload=()=>res(img); img.onerror=()=>res(img); img.src=URL.createObjectURL(file); }); }
function downloadOne(i){
const blob = files[i].compressed;
if(!blob) return alert('No compressed blob yet.');
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'compressed-' + files[i].file.name;
document.body.appendChild(a);
a.click();
a.remove();
}
document.getElementById('compressAllBtn').addEventListener('click', async ()=>{
for(let i=0;i{ files.forEach(f=>URL.revokeObjectURL(f.preview)); files=[]; render(); });
/* small helper to escape text in DOM */
function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); }