(() => { "use strict"; const host = window.chrome && window.chrome.webview; if (!host) { document.body.insertAdjacentHTML( "afterbegin", '
This UI must be run inside the RemoveBG Studio native host.
' ); return; } // ---------------------------------------------------------------- IPC let seq = 0; const pending = new Map(); function call(cmd, params, onProgress) { return new Promise((resolve, reject) => { const id = ++seq; pending.set(id, { resolve, reject, onProgress }); host.postMessage({ id, cmd, params: params || {} }); }); } function callNative(cmd, params) { return new Promise((resolve, reject) => { const id = ++seq; pending.set(id, { resolve, reject }); host.postMessage({ target: "native", id, cmd, params: params || {} }); }); } host.addEventListener("message", (e) => { const data = e.data; if (!data || typeof data !== "object") return; if (data.event === "ready" && data.id === undefined) { setEngineStatus("ready"); return; } if (data.event === "log") { appendLog(data.message, false); return; } const entry = pending.get(data.id); if (!entry) return; if (data.event === "progress") { entry.onProgress && entry.onProgress(data); if (data.message) setStatusOnly(data.message); return; } if (data.event === "done") { pending.delete(data.id); entry.resolve(data.result); return; } if (data.event === "error") { pending.delete(data.id); appendLog(data.message, true); entry.reject(new Error(data.message || "Unknown error")); return; } }); // ---------------------------------------------------------------- chrome / status const engineStatusEl = document.getElementById("engineStatus"); function setEngineStatus(state) { engineStatusEl.className = "rail-status " + state; } const statusText = document.getElementById("statusText"); const progressFill = document.getElementById("progressFill"); const logPanel = document.getElementById("logPanel"); const logToggle = document.getElementById("logToggle"); logToggle.addEventListener("click", () => { logPanel.classList.toggle("is-open"); logToggle.classList.toggle("is-open"); }); function setStatusOnly(message) { if (message) statusText.textContent = message; } function appendLog(message, isError) { if (!message) return; const line = document.createElement("div"); if (isError) line.classList.add("is-error"); line.textContent = message; logPanel.appendChild(line); logPanel.scrollTop = logPanel.scrollHeight; statusText.textContent = message; } function setProgress(current, total) { const pct = total > 0 ? Math.round((current / total) * 100) : 0; progressFill.style.width = pct + "%"; } // ---------------------------------------------------------------- nav const railButtons = document.querySelectorAll(".rail-btn"); const viewTitle = document.getElementById("viewTitle"); const imgModeSegWrap = document.getElementById("imgModeSeg"); const stages = { image: document.getElementById("stage-image"), video: document.getElementById("stage-video") }; const panels = { image: document.getElementById("panel-image"), video: document.getElementById("panel-video") }; const titles = { image: "Image Processor", video: "Video Processor" }; railButtons.forEach((btn) => { btn.addEventListener("click", () => { const view = btn.dataset.view; railButtons.forEach((b) => b.classList.toggle("is-active", b === btn)); Object.keys(stages).forEach((k) => { stages[k].hidden = k !== view; panels[k].hidden = k !== view; }); viewTitle.textContent = titles[view]; imgModeSegWrap.style.visibility = view === "image" ? "visible" : "hidden"; }); }); // ---------------------------------------------------------------- models const MODEL_DESCRIPTIONS = {}; function populateModelSelect(select) { select.innerHTML = ""; for (const id of Object.keys(MODEL_DESCRIPTIONS)) { const opt = document.createElement("option"); opt.value = id; opt.textContent = id; select.appendChild(opt); } } async function loadModels() { try { const result = await call("list_models", {}); Object.assign(MODEL_DESCRIPTIONS, result.models); populateModelSelect(imgModel); populateModelSelect(vidModel); imgModel.value = "u2net"; vidModel.value = "u2net"; updateModelDesc(); } catch (err) { appendLog("Failed to load model list: " + err.message, true); } } // ---------------------------------------------------------------- helpers function basename(p) { if (!p) return ""; const parts = p.split(/[\\/]/); return parts[parts.length - 1]; } function setPathLine(el, path, emptyText) { if (path) { el.textContent = path; el.title = path; el.classList.add("has-value"); } else { el.textContent = emptyText; el.title = ""; el.classList.remove("has-value"); } } function defaultImageOutput(inputPath, forBatch) { if (!inputPath) return ""; if (forBatch) { const dir = inputPath.substring(0, inputPath.lastIndexOf("\\")); return (dir || inputPath) + "\\output_nobg"; } const dot = inputPath.lastIndexOf("."); const base = dot > -1 ? inputPath.substring(0, dot) : inputPath; return base + "_nobg.png"; } function defaultVideoOutput(inputPath, exportFormat) { if (!inputPath) return ""; const dot = inputPath.lastIndexOf("."); const base = dot > -1 ? inputPath.substring(0, dot) : inputPath; if (exportFormat === "png_sequence") return base + "_frames"; const ext = exportFormat === "webm_transparent" ? "webm" : exportFormat === "mp4_solid" ? "mp4" : "mov"; return base + "_nobg." + ext; } function debounce(fn, ms) { let t = null; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; } function setupDropTarget(el, onFiles) { el.addEventListener("dragover", (e) => { e.preventDefault(); }); el.addEventListener("drop", (e) => { e.preventDefault(); const files = Array.from(e.dataTransfer.files || []); const paths = files.map((f) => f.path).filter(Boolean); onFiles(paths.length ? paths : null); }); } function bindSwatchRow(container, onChange) { container.querySelectorAll(".swatch-btn").forEach((btn) => { btn.addEventListener("click", () => { container.querySelectorAll(".swatch-btn").forEach((b) => b.classList.remove("is-active")); btn.classList.add("is-active"); onChange(btn.dataset.bg); }); }); } function bindToggle(el, onChange) { let on = false; el.addEventListener("click", () => { on = !on; el.classList.toggle("is-on", on); onChange(on); }); return () => on; } function bindCompareHandle(stageEl, handleEl) { // Dragging can start anywhere on the image, not just the thin divider // line — the handle is just the visual affordance. This mirrors the // usual before/after slider UX and avoids requiring pixel-precise aim. let dragging = false; const setSplit = (clientX) => { const rect = stageEl.getBoundingClientRect(); const pct = Math.min(100, Math.max(0, ((clientX - rect.left) / rect.width) * 100)); stageEl.style.setProperty("--split", pct.toFixed(2)); }; const start = (e) => { dragging = true; handleEl.classList.add("is-dragging"); stageEl.setPointerCapture(e.pointerId); setSplit(e.clientX); e.preventDefault(); }; const move = (e) => { if (dragging) setSplit(e.clientX); }; const end = (e) => { dragging = false; handleEl.classList.remove("is-dragging"); try { stageEl.releasePointerCapture(e.pointerId); } catch (err) { /* already released */ } }; stageEl.addEventListener("pointerdown", start); stageEl.addEventListener("pointermove", move); stageEl.addEventListener("pointerup", end); stageEl.addEventListener("pointercancel", end); } function bgColorFor(kind, customHex) { return kind === "transparent" ? null : kind === "green" ? "#00FF00" : kind === "white" ? "#FFFFFF" : kind === "black" ? "#000000" : customHex; } // ================================================================== // IMAGE VIEW // ================================================================== const imgModel = document.getElementById("imgModel"); const imgModelDesc = document.getElementById("imgModelDesc"); const imgEmpty = document.getElementById("imgEmpty"); const imgOpenBtn = document.getElementById("imgOpenBtn"); const imgBrowseInput = document.getElementById("imgBrowseInput"); const imgCompare = document.getElementById("imgCompare"); const imgCompareStage = document.getElementById("imgCompareStage"); const imgOriginalImg = document.getElementById("imgOriginalImg"); const imgProcessedImg = document.getElementById("imgProcessedImg"); const imgHandle = document.getElementById("imgHandle"); const imgLoading = document.getElementById("imgLoading"); const imgBatchList = document.getElementById("imgBatchList"); const imgBatchListBody = document.getElementById("imgBatchListBody"); const imgInputPath = document.getElementById("imgInputPath"); const imgOutputPath = document.getElementById("imgOutputPath"); const imgBrowseOutput = document.getElementById("imgBrowseOutput"); const imgFormat = document.getElementById("imgFormat"); const imgFormatField = document.getElementById("imgFormatField"); const imgBgSeg = document.getElementById("imgBgSeg"); const imgSwatch = document.getElementById("imgSwatch"); const imgAlphaToggle = document.getElementById("imgAlphaToggle"); const imgStartBtn = document.getElementById("imgStartBtn"); const stageImageEl = document.getElementById("stage-image"); let imgMode = "single"; let imgInputs = []; let imgOutput = ""; let imgBg = "transparent"; let imgCustomHex = "#00FF00"; const getImgAlpha = bindToggle(imgAlphaToggle, () => queuePreview()); bindCompareHandle(imgCompareStage, imgHandle); document.getElementById("imgModeSeg").querySelectorAll(".seg-btn").forEach((btn) => { btn.addEventListener("click", () => { document.querySelectorAll("#imgModeSeg .seg-btn").forEach((b) => b.classList.remove("is-active")); btn.classList.add("is-active"); imgMode = btn.dataset.mode; imgFormatField.style.display = imgMode === "batch" ? "flex" : "flex"; resetImageInputState(); }); }); function resetImageInputState() { imgInputs = []; imgOutput = ""; setPathLine(imgInputPath, "", "No input selected"); setPathLine(imgOutputPath, "", "Not set"); showImageEmptyState(); } function showImageEmptyState() { imgEmpty.hidden = false; imgCompare.hidden = true; imgLoading.hidden = true; imgBatchList.hidden = true; } bindSwatchRow(imgBgSeg, async (bg) => { imgBg = bg; if (bg === "custom") { const res = await callNative("dialog-pick-color", { initial: imgCustomHex }); if (res.hex) { imgCustomHex = res.hex; imgSwatch.style.background = res.hex; } } queuePreview(); }); imgModel.addEventListener("change", () => { updateModelDesc(); queuePreview(); }); function updateModelDesc() { imgModelDesc.textContent = MODEL_DESCRIPTIONS[imgModel.value] || ""; } async function pickImageInput() { if (imgMode === "single") { const res = await callNative("dialog-open-image", {}); return res.path ? [res.path] : []; } const res = await callNative("dialog-open-images", {}); return res.paths || []; } async function setImageInputs(paths) { if (!paths || !paths.length) return; imgInputs = imgMode === "single" ? [paths[0]] : paths; setPathLine(imgInputPath, imgMode === "single" ? imgInputs[0] : `${imgInputs.length} file(s) selected`, "No input selected"); if (!imgOutput) { imgOutput = defaultImageOutput(imgInputs[0], imgMode === "batch"); setPathLine(imgOutputPath, imgOutput, "Not set"); } if (imgMode === "batch") { imgEmpty.hidden = true; imgCompare.hidden = true; imgLoading.hidden = true; imgBatchList.hidden = false; imgBatchListBody.innerHTML = ""; imgInputs.forEach((p) => { const row = document.createElement("div"); row.className = "batch-row"; row.textContent = basename(p); imgBatchListBody.appendChild(row); }); } else { imgBatchList.hidden = true; runImagePreview(); } } [imgOpenBtn, imgEmpty, imgBrowseInput].forEach((el) => { el.addEventListener("click", async () => setImageInputs(await pickImageInput())); }); setupDropTarget(stageImageEl, (paths) => { if (paths) setImageInputs(paths); else pickImageInput().then(setImageInputs); }); async function runImagePreview() { if (imgMode !== "single" || !imgInputs.length) return; imgEmpty.hidden = true; imgBatchList.hidden = true; imgCompare.hidden = true; imgLoading.hidden = false; try { const result = await call("preview_image", { input: imgInputs[0], model: imgModel.value, alpha_matting: getImgAlpha(), bg_color: bgColorFor(imgBg, imgCustomHex), }); imgOriginalImg.src = "data:image/jpeg;base64," + result.original_b64; imgProcessedImg.src = "data:image/png;base64," + result.preview_b64; imgLoading.hidden = true; imgCompare.hidden = false; } catch (err) { imgLoading.hidden = true; imgEmpty.hidden = false; appendLog("Preview failed: " + err.message, true); } } const queuePreview = debounce(runImagePreview, 260); imgBrowseOutput.addEventListener("click", async () => { if (imgMode === "single") { const suggested = imgInputs[0] ? basename(defaultImageOutput(imgInputs[0], false)) : "output.png"; const res = await callNative("dialog-save-image", { suggestedName: suggested }); if (res.path) { imgOutput = res.path; setPathLine(imgOutputPath, imgOutput, "Not set"); } } else { const res = await callNative("dialog-open-folder", { title: "Select Output Folder" }); if (res.path) { imgOutput = res.path; setPathLine(imgOutputPath, imgOutput, "Not set"); } } }); imgStartBtn.addEventListener("click", async () => { if (!imgInputs.length) { appendLog("Select an input image first.", true); return; } if (!imgOutput) { appendLog("Choose an output destination first.", true); return; } const bgColor = bgColorFor(imgBg, imgCustomHex); imgStartBtn.disabled = true; setEngineStatus("busy"); setProgress(0, 1); try { if (imgMode === "single") { appendLog(`Saving ${basename(imgInputs[0])}…`, false); const result = await call("process_image", { input: imgInputs[0], output: imgOutput, model: imgModel.value, alpha_matting: getImgAlpha(), bg_color: bgColor, }); setProgress(1, 1); appendLog(`Saved → ${result.output}`, false); } else { const result = await call( "process_batch", { inputs: imgInputs, output_dir: imgOutput, model: imgModel.value, alpha_matting: getImgAlpha(), bg_color: bgColor, format: imgFormat.value }, (p) => setProgress(p.current, p.total) ); appendLog(`Batch complete: ${result.count} image(s) saved to ${imgOutput}`, false); } setEngineStatus("ready"); } catch (err) { setEngineStatus("error"); appendLog("Failed: " + err.message, true); } finally { imgStartBtn.disabled = false; } }); // ================================================================== // VIDEO VIEW // ================================================================== const vidModel = document.getElementById("vidModel"); const vidEmpty = document.getElementById("vidEmpty"); const vidOpenBtn = document.getElementById("vidOpenBtn"); const vidBrowseInput = document.getElementById("vidBrowseInput"); const vidCompare = document.getElementById("vidCompare"); const vidCompareStage = document.getElementById("vidCompareStage"); const vidOriginalImg = document.getElementById("vidOriginalImg"); const vidProcessedImg = document.getElementById("vidProcessedImg"); const vidHandle = document.getElementById("vidHandle"); const vidLoading = document.getElementById("vidLoading"); const vidMeta = document.getElementById("vidMeta"); const vidRefreshPreview = document.getElementById("vidRefreshPreview"); const vidInputPath = document.getElementById("vidInputPath"); const vidOutputPath = document.getElementById("vidOutputPath"); const vidBrowseOutput = document.getElementById("vidBrowseOutput"); const vidExportFormat = document.getElementById("vidExportFormat"); const vidBgSeg = document.getElementById("vidBgSeg"); const vidFps = document.getElementById("vidFps"); const vidAlphaToggle = document.getElementById("vidAlphaToggle"); const vidStartBtn = document.getElementById("vidStartBtn"); const stageVideoEl = document.getElementById("stage-video"); let vidInput = ""; let vidOutput = ""; let vidOutputIsDefault = true; // false once the user explicitly browses for output let vidBg = "transparent"; const getVidAlpha = bindToggle(vidAlphaToggle, () => queueVideoPreview()); bindCompareHandle(vidCompareStage, vidHandle); bindSwatchRow(vidBgSeg, (bg) => { vidBg = bg; queueVideoPreview(); }); vidModel.addEventListener("change", () => queueVideoPreview()); vidRefreshPreview.addEventListener("click", () => runVideoPreview()); vidExportFormat.addEventListener("change", () => { // The default suggested output's extension (or folder-vs-file shape) // depends on the export format, so keep it in sync unless the user // has already picked their own destination. if (vidOutputIsDefault && vidInput) { vidOutput = defaultVideoOutput(vidInput, vidExportFormat.value); setPathLine(vidOutputPath, vidOutput, "Not set"); } }); async function pickVideoInput() { const res = await callNative("dialog-open-video", {}); return res.path || null; } async function setVideoInput(path) { if (!path) return; vidInput = path; setPathLine(vidInputPath, vidInput, "No video selected"); if (!vidOutput || vidOutputIsDefault) { vidOutput = defaultVideoOutput(vidInput, vidExportFormat.value); vidOutputIsDefault = true; setPathLine(vidOutputPath, vidOutput, "Not set"); } vidMeta.textContent = "Analyzing…"; try { const info = await call("get_video_info", { path: vidInput }); vidMeta.textContent = `${info.width}×${info.height} · ${info.fps.toFixed(2)} fps · ~${info.frame_count} frames · Audio: ${info.has_audio ? "Yes" : "No"}`; } catch (err) { vidMeta.textContent = "Could not analyze video: " + err.message; } runVideoPreview(); } [vidOpenBtn, vidEmpty, vidBrowseInput].forEach((el) => { el.addEventListener("click", async () => setVideoInput(await pickVideoInput())); }); setupDropTarget(stageVideoEl, (paths) => { if (paths && paths[0]) setVideoInput(paths[0]); else pickVideoInput().then(setVideoInput); }); async function runVideoPreview() { if (!vidInput) return; vidEmpty.hidden = true; vidCompare.hidden = true; vidLoading.hidden = false; try { const result = await call("preview_video_frame", { video: vidInput, model: vidModel.value, alpha_matting: getVidAlpha(), bg_color: bgColorFor(vidBg, null), }); vidOriginalImg.src = "data:image/jpeg;base64," + result.original_b64; vidProcessedImg.src = "data:image/png;base64," + result.preview_b64; vidLoading.hidden = true; vidCompare.hidden = false; } catch (err) { vidLoading.hidden = true; vidEmpty.hidden = false; appendLog("Video preview failed: " + err.message, true); } } const queueVideoPreview = debounce(runVideoPreview, 260); vidBrowseOutput.addEventListener("click", async () => { let res; if (vidExportFormat.value === "png_sequence") { res = await callNative("dialog-open-folder", { title: "Select Output Folder for PNG Sequence" }); } else { const suggested = vidInput ? basename(defaultVideoOutput(vidInput, vidExportFormat.value)) : "output.mov"; res = await callNative("dialog-save-video", { suggestedName: suggested }); } if (res.path) { vidOutput = res.path; vidOutputIsDefault = false; setPathLine(vidOutputPath, vidOutput, "Not set"); } }); vidStartBtn.addEventListener("click", async () => { if (!vidInput) { appendLog("Select a video file first.", true); return; } if (!vidOutput) { appendLog("Choose an output destination first.", true); return; } const bgColor = bgColorFor(vidBg, null); const fps = parseFloat(vidFps.value) || 0; vidStartBtn.disabled = true; setEngineStatus("busy"); setProgress(0, 1); appendLog(`Starting video processing: ${basename(vidInput)}`, false); try { const result = await call( "process_video", { video: vidInput, output: vidOutput, model: vidModel.value, fps: fps > 0 ? fps : null, export_format: vidExportFormat.value, bg_color: bgColor, alpha_matting: getVidAlpha() }, (p) => setProgress(p.current, p.total) ); setEngineStatus("ready"); appendLog(`Video complete → ${result.output}`, false); } catch (err) { setEngineStatus("error"); appendLog("Failed: " + err.message, true); } finally { vidStartBtn.disabled = false; } }); // ---------------------------------------------------------------- boot setEngineStatus(""); loadModels(); })();