import { rgbaToThumbHash } from "thumbhash"; import ExifReader from "exifreader"; import type { ImageMetadata } from "astro"; import { readFile } from "node:fs/promises"; import sharp from "sharp"; export async function generateThumbHash( buffer: ArrayBuffer, ): Promise { if (!buffer) return; const sp = sharp(buffer); const meta = await sp.metadata(); const scaleFactor = 100 / Math.max(meta.width, meta.height); const smallWidth = Math.floor(meta.width * scaleFactor); const smallHeight = Math.floor(meta.height * scaleFactor); const smallImg = await sp .resize(smallWidth, smallHeight) .withMetadata() .raw() .ensureAlpha() .toBuffer(); const hashBuffer = rgbaToThumbHash(smallWidth, smallHeight, smallImg); return Buffer.from(hashBuffer).toString("base64"); } const allowedExif = [ "ApertureValue", "DateTimeOriginal", "ShutterSpeedValue", "ExposureTime", "ApertureValue", "FNumber", "FocalLength", "GPSLatitude", "GPSLongitude", "GPSAltitude", "IsoSpeedRatings", "Make", "Model", ]; export async function getImageBuffer( image: ImageMetadata, ): Promise { if (image.format === "svg") return undefined; // SVGs don't have EXIF data const imagePath = (image as ImageMetadata & { fsPath: string }).fsPath ?? image.src; if (!imagePath) return undefined; try { if (imagePath.startsWith("https://") || imagePath.startsWith("http://")) { const res = await fetch(imagePath, { signal: AbortSignal.timeout(5000) }); return await res.arrayBuffer(); } else { const b = await readFile(imagePath); return b.buffer as ArrayBuffer; } } catch (err) { return undefined; } } export async function getExifData(buffer: ArrayBuffer) { if (!buffer) return undefined; const tags = await ExifReader.load(buffer, { async: true }); const out: Record = {}; let hasExif = false; for (const key of allowedExif) { if (!tags[key]) continue; hasExif = true; out[key] = tags[key]?.description; } return hasExif ? out : undefined; }