2023-08-11 16:13:20 +02:00
|
|
|
import { hash, isLocalImage } from "@lib/string.ts";
|
2023-07-30 18:27:45 +02:00
|
|
|
import * as cache from "@lib/cache/cache.ts";
|
2023-08-11 16:13:20 +02:00
|
|
|
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";
|
2023-08-11 16:13:20 +02:00
|
|
|
import { generateThumbhash } from "@lib/thumbhash.ts";
|
|
|
|
import { SILVERBULLET_SERVER } from "@lib/env.ts";
|
2023-07-30 18:27:45 +02:00
|
|
|
|
|
|
|
type ImageCacheOptions = {
|
|
|
|
url: string;
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
mediaType?: string;
|
2023-08-11 16:13:20 +02:00
|
|
|
suffix?: string;
|
2023-07-30 18:27:45 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const CACHE_KEY = "images";
|
2023-08-05 22:16:14 +02:00
|
|
|
const log = createLogger("cache/image");
|
2023-07-30 18:27:45 +02:00
|
|
|
|
2023-08-11 16:13:20 +02:00
|
|
|
function getCacheKey(
|
|
|
|
{ url: _url, width, height, suffix }: ImageCacheOptions,
|
|
|
|
) {
|
|
|
|
const isLocal = isLocalImage(_url);
|
|
|
|
const url = new URL(isLocal ? `${SILVERBULLET_SERVER}/${_url}` : _url);
|
|
|
|
|
|
|
|
const _suffix = suffix || `${width}:${height}`;
|
|
|
|
|
2023-08-04 22:35:25 +02:00
|
|
|
return `${CACHE_KEY}:${url.hostname}:${
|
|
|
|
url.pathname.replaceAll("/", ":")
|
2023-08-11 16:13:20 +02:00
|
|
|
}:${_suffix}`
|
2023-07-30 18:27:45 +02:00
|
|
|
.replace(
|
2023-08-04 22:35:25 +02:00
|
|
|
"::",
|
|
|
|
":",
|
2023-07-30 18:27:45 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-08-11 16:13:20 +02:00
|
|
|
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;
|
|
|
|
const hash = generateThumbhash(bytes, _image.width, _image.height);
|
|
|
|
if (hash) {
|
|
|
|
cache.set(
|
|
|
|
getCacheKey({
|
|
|
|
url,
|
|
|
|
suffix: "thumbnail",
|
|
|
|
width: _image.width,
|
|
|
|
height: _image.height,
|
|
|
|
}),
|
|
|
|
hash,
|
|
|
|
);
|
|
|
|
res(hash);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} 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);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-08-11 16:13:20 +02:00
|
|
|
export function getThumbhash({ url }: { url: string }) {
|
|
|
|
return cache.get<Uint8Array>(
|
|
|
|
getCacheKey({
|
|
|
|
url,
|
|
|
|
suffix: "thumbnail",
|
|
|
|
width: 200,
|
|
|
|
height: 200,
|
|
|
|
}),
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-07-30 18:27:45 +02:00
|
|
|
export async function getImage({ url, width, height }: ImageCacheOptions) {
|
|
|
|
const cacheKey = getCacheKey({ url, width, height });
|
|
|
|
|
|
|
|
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);
|
2023-07-30 18:27:45 +02:00
|
|
|
if (!imageContent) return;
|
|
|
|
|
|
|
|
return {
|
|
|
|
...pointerCache,
|
|
|
|
buffer: imageContent,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function setImage(
|
|
|
|
buffer: Uint8Array,
|
|
|
|
{ url, width, height, mediaType }: ImageCacheOptions,
|
|
|
|
) {
|
|
|
|
const clone = new Uint8Array(buffer);
|
|
|
|
|
2023-08-02 02:51:20 +02:00
|
|
|
const imageCorrect = await verifyImage(clone);
|
|
|
|
if (!imageCorrect) {
|
2023-08-05 22:16:14 +02:00
|
|
|
log.info("failed to store image", { url });
|
2023-08-02 02:51:20 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-07-30 18:27:45 +02:00
|
|
|
const cacheKey = getCacheKey({ url, width, height });
|
|
|
|
const pointerId = await hash(cacheKey);
|
|
|
|
|
2023-08-04 22:35:25 +02:00
|
|
|
await cache.set(`image:${pointerId}`, clone);
|
2023-08-02 15:56:33 +02:00
|
|
|
cache.expire(pointerId, 60 * 60 * 24);
|
|
|
|
cache.expire(cacheKey, 60 * 60 * 24);
|
2023-07-30 18:27:45 +02:00
|
|
|
|
|
|
|
await cache.set(
|
|
|
|
cacheKey,
|
|
|
|
JSON.stringify({
|
|
|
|
id: pointerId,
|
|
|
|
url,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
mediaType,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|