import { AutoModel, AutoProcessor, env, RawImage } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.1/dist/transformers.min.js'; env.allowLocalModels = false; env.backends.onnx.wasm.proxy = false; document.addEventListener('DOMContentLoaded', () => { const $ = id => document.getElementById(id); const html = document.documentElement; const themeToggle = $('themeToggle'); const iconSun = $('iconSun'); const iconMoon = $('iconMoon'); const uploadZone = $('uploadZone'); const fileInput = $('fileInput'); const panel = $('panel'); const originalImg = $('originalImg'); const resultBody = $('resultBody'); const resultStatus = $('resultStatus'); const processBtn = $('processBtn'); const resetBtn = $('resetBtn'); const progressWrap = $('progressWrap'); const progressFill = $('progressFill'); const progressLabel = $('progressLabel'); const uploadHint = $('uploadHint'); const messageBanner = $('messageBanner'); const queueList = $('queueList'); const historyList = $('historyList'); const downloadZipBtn = $('downloadZipBtn'); const downloadHistoryZipBtn = $('downloadHistoryZipBtn'); const clearHistoryBtn = $('clearHistoryBtn'); const MAX_FILE_SIZE = 20 * 1024 * 1024; const ACCEPTED_TYPES = ['image/png', 'image/jpeg', 'image/webp']; let model = null; let processor = null; let currentFile = null; let currentDataURL = null; let filesQueue = []; let history = []; let currentBatchResults = []; let isDark = false; let isBusy = false; function applyTheme(dark) { isDark = dark; html.setAttribute('data-theme', isDark ? 'dark' : 'light'); iconSun.style.display = isDark ? 'none' : ''; iconMoon.style.display = isDark ? '' : 'none'; } function initTheme() { const savedTheme = localStorage.getItem('theme'); if (savedTheme !== null) { applyTheme(savedTheme === 'dark'); } else { const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; applyTheme(prefersDark); } } function setMessage(text, type = 'info') { if (!text) { messageBanner.className = 'message-banner'; messageBanner.textContent = ''; return; } messageBanner.className = `message-banner visible ${type}`; messageBanner.textContent = text; } function setProgress(pct, label) { progressFill.style.width = pct + '%'; progressLabel.textContent = label; } function resetProgress() { setProgress(0, 'Waiting for image...'); progressWrap.classList.remove('visible'); } function fileToDataURL(file) { return new Promise((resolve, reject) => { try { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.onerror = (err) => reject(err); reader.readAsDataURL(file); } catch (err) { reject(err); } }); } function dataURLtoBlob(dataURL) { const parts = dataURL.split(','); const meta = parts[0] || ''; const bstr = atob(parts[1] || ''); const mimeMatch = meta.match(/:(.*?);/); const mime = mimeMatch ? mimeMatch[1] : 'application/octet-stream'; const n = bstr.length; const u8 = new Uint8Array(n); for (let i = 0; i < n; i++) u8[i] = bstr.charCodeAt(i); return new Blob([u8], { type: mime }); } function imageDataFromDataURL(dataURL) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { const c = document.createElement('canvas'); c.width = img.naturalWidth; c.height = img.naturalHeight; const ctx = c.getContext('2d'); ctx.drawImage(img, 0, 0); resolve({ imageData: ctx.getImageData(0, 0, c.width, c.height), width: c.width, height: c.height }); }; img.onerror = reject; img.src = dataURL; }); } function setUploadState(state) { uploadZone.classList.remove('is-error', 'is-ready', 'is-processing'); if (state) uploadZone.classList.add(`is-${state}`); } function validateFile(file) { if (!file) return { valid: false, message: 'No file was selected.' }; const extension = (file.name || '').split('.').pop()?.toLowerCase(); const normalizedType = (file.type || '').toLowerCase(); const hasSupportedType = ACCEPTED_TYPES.includes(normalizedType) || ['png', 'jpg', 'jpeg', 'webp'].includes(extension); if (!hasSupportedType) { return { valid: false, message: 'Please choose a PNG, JPEG, or WebP image.' }; } if (file.size > MAX_FILE_SIZE) { return { valid: false, message: 'The selected file is larger than 20 MB. Please choose a smaller image.' }; } return { valid: true }; } function resetResultArea() { resultBody.innerHTML = `
Your result will appear here
`; resultBody.style.flexDirection = ''; resultBody.classList.remove('has-result'); resultStatus.style.display = 'none'; resultStatus.className = 'status-badge processing'; resultStatus.style.background = ''; resultStatus.style.color = ''; } function setBusy(busy) { isBusy = busy; processBtn.disabled = busy; fileInput.disabled = busy; uploadZone.classList.toggle('is-processing', busy); } themeToggle.addEventListener('click', () => { const newIsDark = !isDark; applyTheme(newIsDark); localStorage.setItem('theme', newIsDark ? 'dark' : 'light'); }); if (window.matchMedia) { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { if (!localStorage.getItem('theme')) { applyTheme(e.matches); } }); } 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 files = e.dataTransfer.files; if (files && files.length) handleFile(files); }); uploadZone.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); fileInput.click(); } }); fileInput.addEventListener('change', () => { if (fileInput.files && fileInput.files.length) handleFile(fileInput.files); }); async function handleFile(fileOrList) { const files = fileOrList instanceof FileList || Array.isArray(fileOrList) ? Array.from(fileOrList) : [fileOrList]; let added = 0; for (const file of files) { const validation = validateFile(file); if (!validation.valid) { setUploadState('error'); setMessage(validation.message, 'error'); continue; } try { const dataURL = await fileToDataURL(file); filesQueue.push({ file, name: file.name, dataURL }); added++; } catch (err) { console.error('[Upload] error:', err); } } if (filesQueue.length > 0) { // show first item as preview currentFile = filesQueue[0].file; currentDataURL = filesQueue[0].dataURL; originalImg.src = currentDataURL; uploadZone.style.display = 'none'; panel.classList.add('visible'); resetResultArea(); resultStatus.style.display = 'none'; resetProgress(); setMessage(`${filesQueue.length} image(s) added to queue. Click Remove Background to process.`, 'success'); setUploadState('ready'); renderQueue(); updateOriginalLabel(); setBusy(false); } else if (added === 0) { setMessage('No valid images were added.', 'warning'); } } function renderQueue() { if (!queueList) return; queueList.innerHTML = ''; filesQueue.forEach((it, idx) => { const el = document.createElement('div'); el.className = 'item'; el.innerHTML = `${it.name}
${it.name}
✕
`; queueList.appendChild(el); }); // attach remove handlers queueList.querySelectorAll('.remove').forEach(btn => { btn.addEventListener('click', (e) => { const idx = Number(e.currentTarget.getAttribute('data-idx')); filesQueue.splice(idx, 1); renderQueue(); if (filesQueue.length === 0) { // restore upload view panel.classList.remove('visible'); uploadZone.style.display = ''; } else { currentFile = filesQueue[0].file; currentDataURL = filesQueue[0].dataURL; originalImg.src = currentDataURL; } }); }); updateOriginalLabel(); } function loadHistory() { try { const raw = localStorage.getItem('rb_history'); history = raw ? JSON.parse(raw) : []; } catch (e) { history = []; } renderHistory(); } function updateOriginalLabel() { const lbl = document.getElementById('originalLabel'); if (!lbl) return; const count = filesQueue.length || (currentFile ? 1 : 0); lbl.textContent = count > 1 ? `Original (${count})` : 'Original'; } function saveHistory() { try { localStorage.setItem('rb_history', JSON.stringify(history)); } catch (e) { console.warn('Could not save history', e); } } function addToHistory(item) { history.unshift(item); // limit history if (history.length > 40) history.length = 40; saveHistory(); renderHistory(); } function renderHistory() { if (!historyList) return; historyList.innerHTML = ''; history.forEach((h, idx) => { const el = document.createElement('div'); el.className = 'item'; el.innerHTML = `${h.name}
${h.name}
`; el.title = 'Download image'; el.addEventListener('click', () => { // download const a = document.createElement('a'); a.href = h.dataURL; a.download = h.name || `removed-${idx}.png`; a.click(); }); historyList.appendChild(el); }); // show/hide history action buttons if (downloadHistoryZipBtn) downloadHistoryZipBtn.style.display = history.length > 0 ? 'inline-block' : 'none'; if (clearHistoryBtn) clearHistoryBtn.style.display = history.length > 0 ? 'inline-block' : 'none'; } if (downloadHistoryZipBtn) { downloadHistoryZipBtn.addEventListener('click', async () => { if (!history || history.length === 0) return; try { setMessage('Preparing ZIP of history...', 'info'); const zip = new JSZip(); for (const it of history) { zip.file(it.name, dataURLtoBlob(it.dataURL)); } const blob = await zip.generateAsync({ type: 'blob' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'history-removed-images.zip'; a.click(); URL.revokeObjectURL(a.href); setMessage('ZIP downloaded', 'success'); } catch (e) { console.error('ZIP error', e); setMessage('Failed to create ZIP from history.', 'error'); } }); } if (clearHistoryBtn) { clearHistoryBtn.addEventListener('click', () => { try { if (!confirm('Clear history? This cannot be undone.')) return; history = []; saveHistory(); renderHistory(); setMessage('History cleared', 'success'); } catch (e) { console.error('Clear history error', e); setMessage('Failed to clear history.', 'error'); } }); } async function ensureModelReady() { if (model && processor) return; setProgress(8, 'Downloading model files...'); try { model = await AutoModel.from_pretrained('briaai/RMBG-1.4', { config: { model_type: 'custom' }, }); setProgress(36, 'Preparing image processor...'); processor = await AutoProcessor.from_pretrained('briaai/RMBG-1.4', { config: { do_normalize: true, do_pad: false, do_rescale: true, do_resize: true, image_mean: [0.5, 0.5, 0.5], image_std: [1, 1, 1], resample: 2, rescale_factor: 1 / 255, size: { width: 1024, height: 1024 }, }, }); } catch (error) { throw new Error(`The AI model could not be loaded. Please check your connection and try again. ${error?.message || ''}`.trim()); } } async function runSegmentation(dataURL) { await ensureModelReady(); setProgress(46, 'Reading image...'); const image = await RawImage.fromURL(dataURL); const origW = image.width; const origH = image.height; setProgress(58, 'Preparing mask...'); const { pixel_values } = await processor(image); setProgress(74, 'Running inference...'); const { output } = await model({ input: pixel_values }); setProgress(86, 'Refining mask...'); const tensor = output[0]; const dims = tensor.dims; const raw = tensor.data; const tH = dims[dims.length - 2]; const tW = dims[dims.length - 1]; const nPx = tH * tW; const offset = raw.length - nPx; let dMin = Infinity; let dMax = -Infinity; for (let i = 0; i < nPx; i++) { const v = raw[offset + i]; if (v < dMin) dMin = v; if (v > dMax) dMax = v; } const needsSigmoid = dMin < -0.01 || dMax > 1.01; const sigmoid = x => 1 / (1 + Math.exp(-x)); const maskTensor = new Float32Array(nPx); for (let i = 0; i < nPx; i++) { const v = raw[offset + i]; maskTensor[i] = needsSigmoid ? sigmoid(v) : v; } const maskFull = new Float32Array(origW * origH); const scaleX = (tW - 1) / Math.max(origW - 1, 1); const scaleY = (tH - 1) / Math.max(origH - 1, 1); for (let y = 0; y < origH; y++) { const ty = y * scaleY; const ty0 = Math.floor(ty); const ty1 = Math.min(ty0 + 1, tH - 1); const dy = ty - ty0; for (let x = 0; x < origW; x++) { const tx = x * scaleX; const tx0 = Math.floor(tx); const tx1 = Math.min(tx0 + 1, tW - 1); const dx = tx - tx0; const v00 = maskTensor[ty0 * tW + tx0]; const v10 = maskTensor[ty0 * tW + tx1]; const v01 = maskTensor[ty1 * tW + tx0]; const v11 = maskTensor[ty1 * tW + tx1]; maskFull[y * origW + x] = v00 * (1 - dx) * (1 - dy) + v10 * dx * (1 - dy) + v01 * (1 - dx) * dy + v11 * dx * dy; } } let borderSum = 0; let borderCount = 0; for (let x = 0; x < origW; x++) { borderSum += maskFull[x] + maskFull[(origH - 1) * origW + x]; borderCount += 2; } for (let y = 1; y < origH - 1; y++) { borderSum += maskFull[y * origW] + maskFull[y * origW + origW - 1]; borderCount += 2; } const borderMean = borderCount > 0 ? borderSum / borderCount : 0; const isInverted = borderMean > 0.5; if (isInverted) { for (let i = 0; i < maskFull.length; i++) maskFull[i] = 1 - maskFull[i]; } setProgress(94, 'Compositing image...'); const { imageData: origImageData } = await imageDataFromDataURL(dataURL); const origPx = origImageData.data; const canvas = document.createElement('canvas'); canvas.width = origW; canvas.height = origH; const ctx = canvas.getContext('2d'); const out = ctx.createImageData(origW, origH); for (let i = 0; i < origW * origH; i++) { out.data[4 * i] = origPx[4 * i]; out.data[4 * i + 1] = origPx[4 * i + 1]; out.data[4 * i + 2] = origPx[4 * i + 2]; out.data[4 * i + 3] = Math.round(maskFull[i] * 255); } ctx.putImageData(out, 0, 0); return canvas.toDataURL('image/png'); } resetBtn.addEventListener('click', () => { currentFile = null; currentDataURL = null; fileInput.value = ''; originalImg.src = ''; panel.classList.remove('visible'); uploadZone.style.display = ''; uploadZone.classList.remove('drag-over', 'is-processing', 'is-ready', 'is-error'); resetResultArea(); resultStatus.style.display = 'none'; resetProgress(); setMessage('Choose a PNG, JPEG, or WebP image to begin.', 'info'); setBusy(false); uploadHint.textContent = 'Supports PNG, JPEG, and WebP — up to 20 MB.'; filesQueue = []; currentBatchResults = []; if (downloadZipBtn) downloadZipBtn.style.display = 'none'; updateOriginalLabel(); }); processBtn.addEventListener('click', async () => { if (isBusy) return; if ((!currentDataURL || !currentDataURL.length) && filesQueue.length === 0) return; currentBatchResults = []; if (downloadZipBtn) downloadZipBtn.style.display = 'none'; setBusy(true); resultStatus.style.display = 'inline-flex'; resultStatus.className = 'status-badge processing'; resultStatus.innerHTML = '
Processing'; progressWrap.classList.add('visible'); setMessage('Processing images...', 'info'); try { if (filesQueue.length > 0) { const total = filesQueue.length; while (filesQueue.length > 0) { const idx = total - filesQueue.length; const item = filesQueue[0]; setProgress(Math.round((idx / total) * 90) + 5, `Processing ${idx + 1}/${total}...`); const resultDataURL = await runSegmentation(item.dataURL); // show result for this item const resultImg = new Image(); resultImg.src = resultDataURL; resultImg.style.cssText = 'max-width:100%;max-height:320px;object-fit:contain;border-radius:4px;'; const dlBtn = document.createElement('button'); dlBtn.className = 'btn btn-secondary'; dlBtn.innerHTML = ' Download PNG'; dlBtn.style.cssText = 'margin-top:12px;width:fit-content;'; dlBtn.addEventListener('click', () => { const a = document.createElement('a'); a.href = resultDataURL; a.download = (item.name ? item.name.replace(/\.[^.]+$/, '') : 'removed') + '-bg.png'; a.click(); }); resultBody.innerHTML = ''; resultBody.style.flexDirection = 'column'; resultBody.classList.add('has-result'); resultBody.appendChild(resultImg); resultBody.appendChild(dlBtn); resultStatus.innerHTML = '
Ready'; resultStatus.className = 'status-badge ready'; // save to history addToHistory({ name: (item.name ? item.name.replace(/\.[^.]+$/, '') : 'removed') + '.png', dataURL: resultDataURL, ts: Date.now() }); currentBatchResults.push({ name: (item.name ? item.name.replace(/\.[^.]+$/, '') : 'removed') + '.png', dataURL: resultDataURL }); // remove processed item filesQueue.shift(); renderQueue(); } if (currentBatchResults.length > 1 && downloadZipBtn) { downloadZipBtn.style.display = 'inline-flex'; downloadZipBtn.onclick = async () => { try { setMessage('Preparing ZIP...', 'info'); const zip = new JSZip(); for (const it of currentBatchResults) { zip.file(it.name, dataURLtoBlob(it.dataURL)); } const blob = await zip.generateAsync({ type: 'blob' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'removed-images.zip'; a.click(); URL.revokeObjectURL(a.href); setMessage('ZIP downloaded', 'success'); } catch (e) { console.error('ZIP error', e); setMessage('Failed to create ZIP.', 'error'); } }; } else if (downloadZipBtn) { downloadZipBtn.style.display = 'none'; downloadZipBtn.onclick = null; } setProgress(100, 'All done ✓'); setMessage('All images processed and saved to history.', 'success'); } else { // single image setProgress(5, 'Preparing segmentation...'); const resultDataURL = await runSegmentation(currentDataURL); const resultImg = new Image(); resultImg.src = resultDataURL; resultImg.style.cssText = 'max-width:100%;max-height:320px;object-fit:contain;border-radius:4px;'; const dlBtn = document.createElement('button'); dlBtn.className = 'btn btn-secondary'; dlBtn.innerHTML = ' Download PNG'; dlBtn.style.cssText = 'margin-top:12px;width:fit-content;'; dlBtn.addEventListener('click', () => { const a = document.createElement('a'); a.href = resultDataURL; a.download = (currentFile && currentFile.name ? currentFile.name.replace(/\.[^.]+$/, '') : 'removed') + '-bg.png'; a.click(); }); resultBody.innerHTML = ''; resultBody.style.flexDirection = 'column'; resultBody.classList.add('has-result'); resultBody.appendChild(resultImg); resultBody.appendChild(dlBtn); resultStatus.innerHTML = '
Ready'; resultStatus.className = 'status-badge ready'; addToHistory({ name: (currentFile && currentFile.name ? currentFile.name.replace(/\.[^.]+$/, '') : 'removed') + '.png', dataURL: resultDataURL, ts: Date.now() }); setProgress(100, 'Done ✓'); setMessage('Background removed successfully. Download your PNG whenever you want.', 'success'); } } catch (error) { console.error('[RMBG] error:', error); const detail = error?.message || 'Something went wrong while processing the image.'; setProgress(0, '⚠ ' + detail); resultStatus.innerHTML = '
Error'; resultStatus.className = 'status-badge error'; setMessage(`Processing failed: ${detail}`, 'error'); } finally { setBusy(false); progressWrap.classList.remove('visible'); } }); initTheme(); setMessage('Choose a PNG, JPEG, or WebP image to begin.', 'info'); uploadHint.textContent = 'Supports PNG, JPEG, and WebP — up to 20 MB.'; loadHistory(); });