memorium/lib/cache/image.ts

184 lines
4.1 KiB
TypeScript
Raw Normal View History

2023-08-12 18:32:56 +02:00
import { hash, isLocalImage, rgbToHex } from "@lib/string.ts";
import * as cache from "@lib/cache/cache.ts";
import {
ImageMagick,
MagickGeometry,
} from "https://deno.land/x/imagemagick_deno@0.0.25/mod.ts";
2023-08-05 22:16:14 +02:00
import { createLogger } from "@lib/log.ts";
import { generateThumbhash } from "@lib/thumbhash.ts";
import { SILVERBULLET_SERVER } from "@lib/env.ts";
2023-08-12 18:32:56 +02:00
type ImageCacheOptionsBasic = {
url: string;
mediaType?: string;
};
2023-08-12 18:32:56 +02:00
interface ImageCacheOptionsDimensions extends ImageCacheOptionsBasic {
width: number;
height: number;
}
interface ImageCacheOptionsSuffix extends ImageCacheOptionsBasic {
suffix: string;
}
type ImageCacheOptions = ImageCacheOptionsDimensions | ImageCacheOptionsSuffix;
const CACHE_KEY = "images";
2023-08-05 22:16:14 +02:00
const log = createLogger("cache/image");
function getCacheKey(
2023-08-12 18:32:56 +02:00
opts: ImageCacheOptions,
) {
2023-08-12 18:32:56 +02:00
const isLocal = isLocalImage(opts.url);
const url = new URL(
isLocal ? `${SILVERBULLET_SERVER}/${opts.url}` : opts.url,
);
2023-08-12 18:32:56 +02:00
const _suffix = "suffix" in opts
? opts.suffix
: `${opts.width}:${opts.height}`;
2023-08-12 18:32:56 +02:00
const cacheId = `${CACHE_KEY}:${url.hostname}:${
2023-08-04 22:35:25 +02:00
url.pathname.replaceAll("/", ":")
}:${_suffix}`
.replace(
2023-08-04 22:35:25 +02:00
"::",
":",
);
2023-08-12 18:32:56 +02:00
return cacheId;
}
export function createThumbhash(
image: Uint8Array,
url: string,
): Promise<string> {
return new Promise((res, rej) => {
try {
ImageMagick.read(image.slice(), (_image) => {
_image.resize(new MagickGeometry(100, 100));
_image.getPixels((pixels) => {
const bytes = pixels.toByteArray(
0,
0,
_image.width,
_image.height,
"RGBA",
);
if (!bytes) return;
2023-08-12 18:32:56 +02:00
const [hash, average] = generateThumbhash(
bytes,
_image.width,
_image.height,
);
if (average) {
cache.set(
getCacheKey({
url,
suffix: "average",
}),
rgbToHex(average.r, average.g, average.b),
);
}
2023-08-11 17:33:06 +02:00
if (hash) {
2023-08-11 17:33:06 +02:00
const b64 = btoa(String.fromCharCode(...hash));
cache.set(
getCacheKey({
url,
suffix: "thumbnail",
}),
2023-08-11 17:33:06 +02:00
b64,
);
2023-08-11 17:33:06 +02:00
res(b64);
}
});
});
} catch (err) {
rej(err);
}
});
}
2023-08-02 02:51:20 +02:00
function verifyImage(
imageBuffer: Uint8Array,
) {
return new Promise<boolean>((resolve) => {
try {
ImageMagick.read(imageBuffer, (image) => {
resolve(image.height !== 0 && image.width !== 0);
});
} catch (_err) {
resolve(false);
}
});
}
export function getThumbhash({ url }: { url: string }) {
2023-08-12 18:32:56 +02:00
return Promise.all(
[
cache.get<Uint8Array>(
getCacheKey({
url,
suffix: "thumbnail",
}),
),
cache.get<string>(
getCacheKey({
url,
suffix: "average",
}),
),
] as const,
);
}
2023-08-12 18:32:56 +02:00
export async function getImage(opts: ImageCacheOptions) {
const cacheKey = getCacheKey(opts);
const pointerCacheRaw = await cache.get<string>(cacheKey);
if (!pointerCacheRaw) return;
const pointerCache = typeof pointerCacheRaw === "string"
? JSON.parse(pointerCacheRaw)
: pointerCacheRaw;
2023-08-04 22:35:25 +02:00
const imageContent = await cache.get(`image:${pointerCache.id}`, true);
if (!imageContent) return;
return {
...pointerCache,
buffer: imageContent,
};
}
export async function setImage(
buffer: Uint8Array,
2023-08-12 18:32:56 +02:00
opts: ImageCacheOptions,
) {
const clone = new Uint8Array(buffer);
2023-08-02 02:51:20 +02:00
const imageCorrect = await verifyImage(clone);
if (!imageCorrect) {
2023-08-12 18:32:56 +02:00
log.info("failed to store image", { url: opts.url });
2023-08-02 02:51:20 +02:00
return;
}
2023-08-12 18:32:56 +02:00
const cacheKey = getCacheKey(opts);
const pointerId = await hash(cacheKey);
2023-08-12 18:32:56 +02:00
await cache.set(`image:${pointerId}`, clone, { expires: 60 * 60 * 24 });
await cache.set(
cacheKey,
JSON.stringify({
id: pointerId,
2023-08-12 18:32:56 +02:00
...("suffix" in opts
? { suffix: opts.suffix }
: { width: opts.width, height: opts.height }),
}),
{ expires: 60 * 60 * 24 * 7 /* 1 week */ },
);
}