Feat: frame bg removal pipeline for data URLs

This commit is contained in:
2026-03-21 00:36:05 -07:00
parent 0c9fca4cde
commit 98c4d4115e

120
src/services/background.js Normal file
View File

@@ -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;
}