105 lines
2.7 KiB
TypeScript
105 lines
2.7 KiB
TypeScript
import { rgbaToThumbHash } from "thumbhash";
|
|
import ExifReader from "exifreader";
|
|
import type { ImageMetadata } from "astro";
|
|
import sharp from "sharp";
|
|
|
|
export async function generateThumbHash(
|
|
image: ImageMetadata & { fsPath?: string },
|
|
) {
|
|
const scaleFactor = 100 / Math.max(image.width, image.height);
|
|
|
|
let smallWidth = Math.floor(image.width * scaleFactor);
|
|
let smallHeight = Math.floor(image.height * scaleFactor);
|
|
|
|
try {
|
|
const imagePath = (image as ImageMetadata & { fsPath: string }).fsPath ??
|
|
image.src;
|
|
|
|
if (!imagePath) return;
|
|
|
|
let sp: ReturnType<typeof sharp>;
|
|
if (imagePath.startsWith("https://") || imagePath.startsWith("http://")) {
|
|
const res = await fetch(imagePath);
|
|
if (!res.ok) {
|
|
return;
|
|
}
|
|
sp = sharp(await res.arrayBuffer());
|
|
} else {
|
|
sp = sharp(imagePath);
|
|
}
|
|
|
|
if (!smallWidth || !smallHeight) {
|
|
const meta = await sp.metadata();
|
|
const scaleFactor = 100 / Math.max(meta.width, meta.height);
|
|
smallWidth = Math.floor(meta.width * scaleFactor);
|
|
smallHeight = Math.floor(meta.height * scaleFactor);
|
|
}
|
|
|
|
const smallImg = await sp
|
|
.resize(smallWidth, smallHeight)
|
|
.withMetadata()
|
|
.raw()
|
|
.ensureAlpha()
|
|
.toBuffer();
|
|
|
|
const buffer = rgbaToThumbHash(smallWidth, smallHeight, smallImg);
|
|
return Buffer.from(buffer).toString("base64");
|
|
} catch (error) {
|
|
console.log(
|
|
`Could not generate thumbhash for ${image.fsPath ?? image.src}`,
|
|
error,
|
|
);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
const allowedExif = [
|
|
"ApertureValue",
|
|
"DateTimeOriginal",
|
|
"ShutterSpeedValue",
|
|
"ExposureTime",
|
|
"ApertureValue",
|
|
"FNumber",
|
|
"FocalLength",
|
|
"GPSLatitude",
|
|
"GPSLongitude",
|
|
"GPSAltitude",
|
|
"IsoSpeedRatings",
|
|
"Make",
|
|
"Model",
|
|
];
|
|
|
|
export async function getExifData(image: ImageMetadata) {
|
|
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 {
|
|
let buffer: ArrayBuffer;
|
|
if (imagePath.startsWith("https://") || imagePath.startsWith("http://")) {
|
|
const res = await fetch(imagePath);
|
|
buffer = await res.arrayBuffer();
|
|
} else {
|
|
buffer = await sharp(imagePath).toBuffer() as unknown as ArrayBuffer;
|
|
}
|
|
|
|
const tags = await ExifReader.load(buffer, { async: true });
|
|
|
|
const out: Record<string, any> = {};
|
|
let hasExif = false;
|
|
|
|
for (const key of allowedExif) {
|
|
if (!tags[key]) continue;
|
|
hasExif = true;
|
|
out[key] = tags[key]?.description;
|
|
}
|
|
|
|
return hasExif ? out : undefined;
|
|
} catch (error) {
|
|
console.log(`Error reading EXIF data from ${JSON.stringify(image)}`, error);
|
|
return undefined;
|
|
}
|
|
}
|