import sharp from 'sharp'; import { rgbaToThumbHash } from 'thumbhash'; const FULL_MAX = 1000; const THUMB_MAX = 200; export default async function processAvifImage(inputBuffer) { const input = Buffer.isBuffer(inputBuffer) ? inputBuffer : Buffer.from(inputBuffer); const meta = await sharp(input).metadata(); const w = meta.width ?? 0; const h = meta.height ?? 0; const isAvif = meta.format === 'avif' || (meta.format === 'heif' && meta.compression === 'av1'); const useOriginalFull = isAvif && w > 0 && h > 0 && w <= FULL_MAX && h <= FULL_MAX; if (!useOriginalFull) { console.log( `processAvifImage: converting full image (format=${meta.format ?? 'unknown'}, ${w}×${h}, avif=${isAvif})`, ); } const thumbPipeline = sharp(input) .resize(THUMB_MAX, THUMB_MAX, { fit: 'inside', withoutEnlargement: true }); const [full, thumb, thumbRgba] = await Promise.all([ useOriginalFull ? Promise.resolve({ info: { width: w, height: h }, data: input, }) : sharp(input) .resize(FULL_MAX, FULL_MAX, { fit: 'inside', withoutEnlargement: true }) .avif({ effort: 4 }) .toBuffer({ resolveWithObject: true }), thumbPipeline.clone().avif({ effort: 4 }).toBuffer({ resolveWithObject: true }), thumbPipeline .clone() .resize(100, 100, { fit: 'inside', withoutEnlargement: true }) .ensureAlpha() .raw() .toBuffer({ resolveWithObject: true }), ]); const thumbHashBytes = rgbaToThumbHash( thumbRgba.info.width, thumbRgba.info.height, thumbRgba.data, ); return { full: { w: full.info.width, h: full.info.height, size: full.data.length, buffer: full.data, }, thumb: { w: thumb.info.width, h: thumb.info.height, size: thumb.data.length, buffer: thumb.data, hash: Buffer.from(thumbHashBytes).toString('base64'), }, }; }