// server/utils/blurHash.ts import sharp from 'sharp' import { encode } from 'blurhash' import { $fetch } from 'ofetch' import { mkdir, writeFile, unlink } from 'node:fs/promises' import { join } from 'node:path' import { tmpdir } from 'node:os' import { randomUUID } from 'node:crypto' // Dossier temporaire pour les images téléchargées const TMP_DIR = join(tmpdir(), 'evilspins-images') async function ensureTmpDir() { try { await mkdir(TMP_DIR, { recursive: true }) } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error } } async function downloadImage(url: string): Promise { await ensureTmpDir() const tmpPath = join(TMP_DIR, `${randomUUID()}.jpg`) try { const response = await $fetch(url, { responseType: 'arrayBuffer', headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', Accept: 'image/*,*/*;q=0.8' }, // Timeout plus long timeout: 60000, // 60 secondes // Désactiver la compression pour les images headers: { 'Accept-Encoding': 'identity' } }) await writeFile(tmpPath, Buffer.from(response)) return tmpPath } catch (error) { // Nettoyer en cas d'erreur try { await unlink(tmpPath).catch(() => {}) } catch { /* Ignorer les erreurs de suppression */ } console.error(`Failed to download image from ${url}:`, error.message) throw error } } function createDefaultBlurhash() { // Un blurhash plus discret return 'L5H2EC=H00~q^-=wD6xuxvV@%KxZ' } export async function generateBlurhash( input: Buffer | string, componentX: number = 4, componentY: number = 3 ): Promise { let tmpPath: string | null = null try { if (typeof input === 'string') { if (input.startsWith('http')) { try { tmpPath = await downloadImage(input) input = tmpPath } catch (error) { console.warn(`Using default blurhash for ${input} due to download error`) return createDefaultBlurhash() } } // Vérifier si le fichier existe try { const image = sharp(input).resize(32, 32, { fit: 'inside' }).ensureAlpha() const { data, info } = await image.raw().toBuffer({ resolveWithObject: true }) return encode(new Uint8ClampedArray(data), info.width, info.height, componentX, componentY) } catch (error) { console.warn(`Error processing image ${input}:`, error.message) return createDefaultBlurhash() } } else { // Si c'est déjà un Buffer try { const image = sharp(input).resize(32, 32, { fit: 'inside' }).ensureAlpha() const { data, info } = await image.raw().toBuffer({ resolveWithObject: true }) return encode(new Uint8ClampedArray(data), info.width, info.height, componentX, componentY) } catch (error) { console.warn('Error processing image buffer:', error.message) return createDefaultBlurhash() } } } catch (error) { console.error('Unexpected error in generateBlurhash:', error.message) return createDefaultBlurhash() } finally { // Nettoyer le fichier temporaire s'il existe if (tmpPath) { try { await unlink(tmpPath).catch(() => {}) } catch { /* Ignorer les erreurs de suppression */ } } } }