/** * Suppression de fond par IA — @imgly/background-removal (WebAssembly). * Le modèle est téléchargé depuis staticimgly.com au premier lancement. */ const CDN_VERSION = '1.7.0'; const CDN = `https://cdn.jsdelivr.net/npm/@imgly/background-removal@${CDN_VERSION}/dist/`; let _removeBg = null; async function loadLib() { if (_removeBg) return _removeBg; const mod = await import(CDN + 'index.mjs'); _removeBg = mod.removeBackground; return _removeBg; } /** * Supprime le fond d'un Blob image via IA puis composite sur blanc. * @param {Blob} blob — image source (JPEG) * @param {function} [onProgress] — callback({ key, percent }) * @param {number} [feather=2] — récupération de bords en px (0 = strict, 5 = doux) * @returns {Promise} Blob JPEG fond blanc */ export async function removeBackground(blob, onProgress, feather = 2) { const removeBg = await loadLib(); const pngBlob = await removeBg(blob, { // publicPath omis → utilise staticimgly.com par défaut output: { format: 'image/png', type: 'foreground', quality: 1, }, progress: (key, current, total) => { if (onProgress && total > 0) { onProgress({ key, percent: Math.round((current / total) * 100) }); } }, }); return compositeOnWhite(pngBlob, feather); } /** * Composite un PNG transparent sur fond blanc. * Si feather > 0, dilate légèrement le contour du sujet (récupère les bords * que l'IA a pu couper trop agressivement). * @param {Blob} pngBlob * @param {number} feather — rayon de dilation en pixels (0–5) * @returns {Promise} JPEG fond blanc */ async function compositeOnWhite(pngBlob, feather) { const objectUrl = URL.createObjectURL(pngBlob); return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { const { width, height } = img; // ── Canvas intermédiaire : premier plan avec bords optionnels ── const fgCanvas = document.createElement('canvas'); fgCanvas.width = width; fgCanvas.height = height; const fgCtx = fgCanvas.getContext('2d'); // 1. Dessin net du premier plan fgCtx.drawImage(img, 0, 0); if (feather > 0) { // 2. On dessine une version floutée DERRIÈRE le dessin net // (destination-over = "derrière le contenu existant") // → les zones transparentes proches des bords du sujet deviennent // semi-transparentes, récupérant les détails coupés par l'IA. fgCtx.globalCompositeOperation = 'destination-over'; fgCtx.filter = `blur(${feather}px)`; fgCtx.drawImage(img, 0, 0); fgCtx.filter = 'none'; fgCtx.globalCompositeOperation = 'source-over'; } // ── Canvas final : fond blanc + premier plan ─────────────────── const out = document.createElement('canvas'); out.width = width; out.height = height; const ctx = out.getContext('2d'); ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, width, height); ctx.drawImage(fgCanvas, 0, 0); URL.revokeObjectURL(objectUrl); out.toBlob(b => { b ? resolve(b) : reject(new Error('canvas.toBlob a retourné null')); }, 'image/jpeg', 0.92); }; img.onerror = () => reject(new Error('Impossible de charger le résultat IA')); img.src = objectUrl; }); }