feat: ZIP import, rotation, feather slider, lightbox + config déploiement
- 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>
This commit is contained in:
+50
-22
@@ -1,13 +1,11 @@
|
||||
/**
|
||||
* Suppression de fond par IA — @imgly/background-removal (WebAssembly).
|
||||
* Le modèle (~40 Mo) est téléchargé depuis le CDN au premier lancement
|
||||
* puis mis en cache automatiquement dans le navigateur.
|
||||
* 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/`;
|
||||
|
||||
// Chargement paresseux de la librairie (évite de bloquer le rendu initial)
|
||||
let _removeBg = null;
|
||||
|
||||
async function loadLib() {
|
||||
@@ -18,18 +16,17 @@ async function loadLib() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime le fond d'un Blob image via IA.
|
||||
* Retourne un nouveau Blob JPEG avec fond blanc pur.
|
||||
* 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) {
|
||||
export async function removeBackground(blob, onProgress, feather = 2) {
|
||||
const removeBg = await loadLib();
|
||||
|
||||
// Détourage IA → PNG avec transparence
|
||||
const pngBlob = await removeBg(blob, {
|
||||
// publicPath omis → utilise le CDN par défaut de img.ly (staticimgly.com)
|
||||
// publicPath omis → utilise staticimgly.com par défaut
|
||||
output: {
|
||||
format: 'image/png',
|
||||
type: 'foreground',
|
||||
@@ -42,32 +39,63 @@ export async function removeBackground(blob, onProgress) {
|
||||
},
|
||||
});
|
||||
|
||||
// Composite sur fond blanc → JPEG
|
||||
return compositeOnWhite(pngBlob);
|
||||
return compositeOnWhite(pngBlob, feather);
|
||||
}
|
||||
|
||||
/**
|
||||
* Composite un PNG transparent sur fond blanc, retourne un Blob JPEG.
|
||||
* 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
|
||||
* @returns {Promise<Blob>}
|
||||
* @param {number} feather — rayon de dilation en pixels (0–5)
|
||||
* @returns {Promise<Blob>} JPEG fond blanc
|
||||
*/
|
||||
async function compositeOnWhite(pngBlob) {
|
||||
async function compositeOnWhite(pngBlob, feather) {
|
||||
const objectUrl = URL.createObjectURL(pngBlob);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
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, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toBlob(b => {
|
||||
URL.revokeObjectURL(img.src);
|
||||
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 = URL.createObjectURL(pngBlob);
|
||||
img.src = objectUrl;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user