99 lines
3.0 KiB
TypeScript
99 lines
3.0 KiB
TypeScript
const CDN_URL = 'https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.7.0/dist/index.mjs'
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
let _removeBg: ((blob: Blob, opts: Record<string, unknown>) => Promise<Blob>) | null = null
|
||
|
||
async function loadLib() {
|
||
if (_removeBg) return _removeBg
|
||
const mod = await import(/* webpackIgnore: true */ CDN_URL)
|
||
_removeBg = mod.removeBackground
|
||
return _removeBg!
|
||
}
|
||
|
||
export interface ProgressEvent {
|
||
key: string
|
||
percent: number
|
||
}
|
||
|
||
/**
|
||
* Supprime le fond d'un Blob image via @imgly IA.
|
||
* Retourne un PNG avec fond transparent (à combiner avec compositeOnWhite).
|
||
*/
|
||
export async function removeBackgroundRaw(
|
||
blob: Blob,
|
||
onProgress?: (e: ProgressEvent) => void,
|
||
): Promise<Blob> {
|
||
const removeBg = await loadLib()
|
||
return removeBg(blob, {
|
||
output: { format: 'image/png', type: 'foreground', quality: 1 },
|
||
progress: (key: string, current: number, total: number) => {
|
||
if (onProgress && total > 0) {
|
||
onProgress({ key, percent: Math.round((current / total) * 100) })
|
||
}
|
||
},
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Composite un PNG transparent sur fond blanc avec feather optionnel.
|
||
* @param pngBlob — PNG avec transparence (résultat de removeBackgroundRaw)
|
||
* @param feather — récupération des bords en px (0 = strict, 5 = doux)
|
||
*/
|
||
export function compositeOnWhite(pngBlob: Blob, feather: number): Promise<Blob> {
|
||
const objectUrl = URL.createObjectURL(pngBlob)
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image()
|
||
|
||
img.onload = () => {
|
||
const { width, height } = img
|
||
|
||
const fgCanvas = document.createElement('canvas')
|
||
fgCanvas.width = width
|
||
fgCanvas.height = height
|
||
const fgCtx = fgCanvas.getContext('2d')!
|
||
|
||
fgCtx.drawImage(img, 0, 0)
|
||
|
||
if (feather > 0) {
|
||
fgCtx.globalCompositeOperation = 'destination-over'
|
||
fgCtx.filter = `blur(${feather}px)`
|
||
fgCtx.drawImage(img, 0, 0)
|
||
fgCtx.filter = 'none'
|
||
fgCtx.globalCompositeOperation = 'source-over'
|
||
}
|
||
|
||
// Canvas de sortie 1000×1000 blanc, sujet centré avec padding de 5%
|
||
const SIZE = 1000
|
||
const PADDING = 0.05
|
||
const available = Math.round(SIZE * (1 - PADDING * 2))
|
||
const scale = Math.min(available / width, available / height)
|
||
const dw = Math.round(width * scale)
|
||
const dh = Math.round(height * scale)
|
||
const dx = Math.round((SIZE - dw) / 2)
|
||
const dy = Math.round((SIZE - dh) / 2)
|
||
|
||
const out = document.createElement('canvas')
|
||
out.width = SIZE
|
||
out.height = SIZE
|
||
const ctx = out.getContext('2d')!
|
||
ctx.fillStyle = '#ffffff'
|
||
ctx.fillRect(0, 0, SIZE, SIZE)
|
||
ctx.drawImage(fgCanvas, dx, dy, dw, dh)
|
||
|
||
URL.revokeObjectURL(objectUrl)
|
||
out.toBlob(
|
||
(b) => (b ? resolve(b) : reject(new Error('canvas.toBlob a retourné null'))),
|
||
'image/jpeg',
|
||
0.92,
|
||
)
|
||
}
|
||
|
||
img.onerror = () => {
|
||
URL.revokeObjectURL(objectUrl)
|
||
reject(new Error('Impossible de charger le PNG'))
|
||
}
|
||
img.src = objectUrl
|
||
})
|
||
}
|