diff --git a/src/services/background.js b/src/services/background.js new file mode 100644 index 0000000..ea47e8d --- /dev/null +++ b/src/services/background.js @@ -0,0 +1,120 @@ +const LIB_URLS = [ + 'https://esm.sh/@imgly/background-removal@1.7.0?bundle', + 'https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.7.0/+esm' +]; + +let removeBackgroundFn = null; +let loadingPromise = null; + +async function loadLib() { + if (removeBackgroundFn) return removeBackgroundFn; + if (loadingPromise) return loadingPromise; + + loadingPromise = (async () => { + let lastErr = null; + for (const url of LIB_URLS) { + try { + const mod = await import(url); + const fn = mod?.default || mod?.removeBackground || mod?.imglyRemoveBackground; + if (typeof fn !== 'function') throw new Error(`No removeBackground export from ${url}`); + removeBackgroundFn = fn; + return removeBackgroundFn; + } catch (err) { + lastErr = err; + } + } + throw lastErr || new Error('Failed to load background-removal library.'); + })(); + + return loadingPromise; +} + +function loadImage(src) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.decoding = 'async'; + img.onload = () => resolve(img); + img.onerror = () => reject(new Error('Failed to decode frame image')); + img.src = src; + }); +} + +async function toPngFile(src, name = 'frame.png') { + const img = await loadImage(src); + const w = img.naturalWidth || img.width; + const h = img.naturalHeight || img.height; + + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('Could not create 2D canvas context'); + + ctx.clearRect(0, 0, w, h); + ctx.drawImage(img, 0, 0, w, h); + + const blob = await new Promise((resolve, reject) => { + canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('Failed converting frame to PNG'))), 'image/png', 1); + }); + + return new File([blob], name, { type: 'image/png' }); +} + +function blobToDataUrl(blob) { + return new Promise((resolve, reject) => { + const fr = new FileReader(); + fr.onload = () => resolve(String(fr.result || '')); + fr.onerror = () => reject(new Error('Failed to convert blob to data URL')); + fr.readAsDataURL(blob); + }); +} + +export async function removeBackgroundFrames(frameDataUrls, { onProgress } = {}) { + if (!Array.isArray(frameDataUrls) || !frameDataUrls.length) throw new Error('No frames for background removal.'); + const removeBackground = await loadLib(); + + const out = []; + const total = frameDataUrls.length; + + for (let i = 0; i < total; i++) { + const frameNo = i + 1; + const src = frameDataUrls[i]; + + onProgress?.({ + pct: Math.round((i / total) * 100), + text: `preparing frame ${frameNo} of ${total} for background removal...` + }); + + const pngFile = await toPngFile(src, `frame-${frameNo}.png`); + + onProgress?.({ + pct: Math.round((i / total) * 100), + text: `removing background from frame ${frameNo} of ${total}...` + }); + + const outBlob = await removeBackground(pngFile, { + debug: false, + output: { type: 'foreground', format: 'image/png', quality: 1 }, + progress: (_key, current, stepTotal) => { + if (!stepTotal) return; + const inner = current / stepTotal; + const pct = Math.round(((i + inner) / total) * 100); + onProgress?.({ + pct, + text: `removing background from frame ${frameNo} of ${total}...` + }); + } + }); + + const outDataUrl = await blobToDataUrl(outBlob); + out.push(outDataUrl); + + onProgress?.({ + pct: Math.round((frameNo / total) * 100), + text: `frame ${frameNo} of ${total} background removed` + }); + } + + onProgress?.({ pct: 100, text: 'background removal done' }); + return out; +}