114 lines
3.9 KiB
TypeScript
114 lines
3.9 KiB
TypeScript
/**
|
|
* Convertit un Blob HEIC/HEIF en Blob JPEG.
|
|
* Tente d'abord la conversion native du navigateur (instantané sur Safari/macOS),
|
|
* puis repasse sur heic2any si le natif échoue (Chrome, Firefox).
|
|
* À utiliser uniquement côté client (navigateur).
|
|
*/
|
|
export async function convertHeicToJpeg(blob: Blob): Promise<Blob> {
|
|
if (!(blob instanceof Blob)) {
|
|
throw new TypeError("L'entrée doit être un Blob")
|
|
}
|
|
|
|
const type = blob.type.toLowerCase()
|
|
if (type !== '' && !['image/heic', 'image/heif'].includes(type)) {
|
|
throw new TypeError(`Format attendu : HEIC/HEIF. Reçu : ${blob.type || '(vide)'}`)
|
|
}
|
|
|
|
// Méthode 1 : conversion native (Safari 14+, macOS Chrome 105+)
|
|
try {
|
|
const result = await convertNative(blob)
|
|
console.log('[heicConverter] ✅ Conversion native réussie')
|
|
return result
|
|
} catch (e) {
|
|
console.warn(`[heicConverter] Conversion native échouée (${(e as Error).message}), repasse sur heic2any…`)
|
|
}
|
|
|
|
// Méthode 2 : conversion serveur via /api/images/convert-heic (sharp, fiable sur tous les navigateurs)
|
|
try {
|
|
const result = await convertViaServer(blob)
|
|
console.log('[heicConverter] ✅ Conversion serveur réussie')
|
|
return result
|
|
} catch (e) {
|
|
console.warn(`[heicConverter] Conversion serveur échouée (${(e as Error).message}), repasse sur heic2any…`)
|
|
}
|
|
|
|
// Méthode 3 : heic2any fallback (plus lent, ~10-30s)
|
|
const heic2any = (await import('heic2any')).default
|
|
|
|
const timeout = new Promise<never>((_, reject) =>
|
|
setTimeout(
|
|
() => reject(new Error('Timeout conversion HEIC (30s). Essayez de convertir le fichier en JPG avant de le déposer.')),
|
|
30_000,
|
|
),
|
|
)
|
|
|
|
const conversion = heic2any({ blob, toType: 'image/jpeg', quality: 0.92 })
|
|
const result = await Promise.race([conversion, timeout])
|
|
const output = Array.isArray(result) ? result[0] : result
|
|
|
|
if (!(output instanceof Blob)) {
|
|
throw new Error("heic2any n'a pas retourné un Blob")
|
|
}
|
|
|
|
console.log('[heicConverter] ✅ Conversion heic2any réussie')
|
|
return output
|
|
}
|
|
|
|
async function convertNative(blob: Blob): Promise<Blob> {
|
|
const typed = blob.type ? blob : new Blob([blob], { type: 'image/heic' })
|
|
|
|
// Tentative 1 : createImageBitmap (Safari 14+)
|
|
try {
|
|
const bitmap = await createImageBitmap(typed)
|
|
const canvas = document.createElement('canvas')
|
|
canvas.width = bitmap.width
|
|
canvas.height = bitmap.height
|
|
canvas.getContext('2d')!.drawImage(bitmap, 0, 0)
|
|
bitmap.close()
|
|
return await canvasToBlob(canvas)
|
|
} catch {
|
|
// Pas supporté — essai suivant
|
|
}
|
|
|
|
// Tentative 2 : <img> + canvas (Chrome sur macOS — utilise le décodeur HEIC natif de macOS)
|
|
return new Promise<Blob>((resolve, reject) => {
|
|
const url = URL.createObjectURL(typed)
|
|
const img = new Image()
|
|
img.onload = () => {
|
|
const canvas = document.createElement('canvas')
|
|
canvas.width = img.naturalWidth
|
|
canvas.height = img.naturalHeight
|
|
canvas.getContext('2d')!.drawImage(img, 0, 0)
|
|
URL.revokeObjectURL(url)
|
|
canvasToBlob(canvas).then(resolve).catch(reject)
|
|
}
|
|
img.onerror = () => {
|
|
URL.revokeObjectURL(url)
|
|
reject(new Error('Décodage HEIC non supporté par ce navigateur'))
|
|
}
|
|
img.src = url
|
|
})
|
|
}
|
|
|
|
async function convertViaServer(blob: Blob): Promise<Blob> {
|
|
const formData = new FormData()
|
|
const file = blob instanceof File ? blob : new File([blob], 'image.heic', { type: 'image/heic' })
|
|
formData.append('file', file)
|
|
const res = await fetch('/api/images/convert-heic', { method: 'POST', body: formData })
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
|
throw new Error(err.error ?? `HTTP ${res.status}`)
|
|
}
|
|
return res.blob()
|
|
}
|
|
|
|
function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
|
|
return new Promise((resolve, reject) => {
|
|
canvas.toBlob(
|
|
b => (b ? resolve(b) : reject(new Error('canvas.toBlob a retourné null'))),
|
|
'image/jpeg',
|
|
0.92,
|
|
)
|
|
})
|
|
}
|