import { hash, isLocalImage } 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"; import { createLogger } from "@lib/log.ts"; import { generateThumbhash } from "@lib/thumbhash.ts"; import { SILVERBULLET_SERVER } from "@lib/env.ts"; type ImageCacheOptions = { url: string; width: number; height: number; mediaType?: string; suffix?: string; }; const CACHE_KEY = "images"; const log = createLogger("cache/image"); 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}`; return `${CACHE_KEY}:${url.hostname}:${ url.pathname.replaceAll("/", ":") }:${_suffix}` .replace( "::", ":", ); } export function createThumbhash( image: Uint8Array, url: string, ): Promise { 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) { const b64 = btoa(String.fromCharCode(...hash)); cache.set( getCacheKey({ url, suffix: "thumbnail", width: _image.width, height: _image.height, }), b64, ); res(b64); } }); }); } catch (err) { rej(err); } }); } function verifyImage( imageBuffer: Uint8Array, ) { return new Promise((resolve) => { try { ImageMagick.read(imageBuffer, (image) => { resolve(image.height !== 0 && image.width !== 0); }); } catch (_err) { resolve(false); } }); } export function getThumbhash({ url }: { url: string }) { return cache.get( getCacheKey({ url, suffix: "thumbnail", width: 200, height: 200, }), ); } export async function getImage({ url, width, height }: ImageCacheOptions) { const cacheKey = getCacheKey({ url, width, height }); const pointerCacheRaw = await cache.get(cacheKey); if (!pointerCacheRaw) return; const pointerCache = typeof pointerCacheRaw === "string" ? JSON.parse(pointerCacheRaw) : pointerCacheRaw; const imageContent = await cache.get(`image:${pointerCache.id}`, true); if (!imageContent) return; return { ...pointerCache, buffer: imageContent, }; } export async function setImage( buffer: Uint8Array, { url, width, height, mediaType }: ImageCacheOptions, ) { const clone = new Uint8Array(buffer); const imageCorrect = await verifyImage(clone); if (!imageCorrect) { log.info("failed to store image", { url }); return; } const cacheKey = getCacheKey({ url, width, height }); const pointerId = await hash(cacheKey); await cache.set(`image:${pointerId}`, clone); cache.expire(pointerId, 60 * 60 * 24); cache.expire(cacheKey, 60 * 60 * 24); await cache.set( cacheKey, JSON.stringify({ id: pointerId, url, width, height, mediaType, }), ); }