86d9b351b1
- Import ZIP produits (HEIC/JPG/PNG, structure dossiers conservée) - Rotation des images ±90° avec aperçu CSS et export canvas - Slider récupération des bords (feather 0–5 px) - Lightbox plein écran (clic miniature, Échap, fond noir) - Fix timeout HEIC 30s → 120s - Fix MIME type blobs JSZip pour heic2any - Dockerfile nginx:alpine + docker-compose.yml pour déploiement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
102 lines
3.4 KiB
JavaScript
102 lines
3.4 KiB
JavaScript
/**
|
||
* 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>} 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<Blob>} 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;
|
||
});
|
||
}
|