import { HandlerContext } from "$fresh/server.ts"; import { ImageMagick, initializeImageMagick, MagickGeometry, } from "https://deno.land/x/imagemagick_deno@0.0.14/mod.ts"; import { parseMediaType } from "https://deno.land/std@0.175.0/media_types/parse_media_type.ts"; import * as cache from "@lib/cache/image.ts"; await initializeImageMagick(); async function getRemoteImage(image: string) { console.log("[api/image] fetching", { image }); const sourceRes = await fetch(image); if (!sourceRes.ok) { return "Error retrieving image from URL."; } const mediaType = parseMediaType(sourceRes.headers.get("Content-Type")!)[0]; if (mediaType.split("/")[0] !== "image") { return "URL is not image type."; } return { buffer: new Uint8Array(await sourceRes.arrayBuffer()), mediaType, }; } function getWidthHeight( current: { width: number; height: number }, final: { width: number; height: number }, ) { const ratio = (current.width / final.width) > (current.height / final.height) ? (current.height / final.height) : (current.width / final.width); return new MagickGeometry( current.width / ratio, current.height / ratio, ); } function modifyImage( imageBuffer: Uint8Array, params: { width: number; height: number; mode: "resize" | "crop" }, ) { return new Promise((resolve) => { ImageMagick.read(imageBuffer, (image) => { const sizingData = getWidthHeight(image, params); if (params.mode === "resize") { image.resize(sizingData); } else { image.crop(sizingData); } image.write((data) => resolve(data)); }); }); } function parseParams(reqUrl: URL) { const image = reqUrl.searchParams.get("image")?.replace(/^\//, ""); if (image == null) { return "Missing 'image' query parameter."; } const height = Number(reqUrl.searchParams.get("height")) || 0; const width = Number(reqUrl.searchParams.get("width")) || 0; if (height === 0 && width === 0) { //return "Missing non-zero 'height' or 'width' query parameter."; } if (height < 0 || width < 0) { return "Negative height or width is not supported."; } const maxDimension = 2048; if (height > maxDimension || width > maxDimension) { return `Width and height cannot exceed ${maxDimension}.`; } return { image, height, width, }; } export const handler = async ( _req: Request, _ctx: HandlerContext, ): Promise => { const url = new URL(_req.url); const params = parseParams(url); if (typeof params === "string") { return new Response(params, { status: 400 }); } const imageUrl = Deno.env.get("SILVERBULLET_SERVER") + "/" + params.image; console.log("[api/image] " + imageUrl); const cachedResponse = await cache.getImage({ url: imageUrl, width: params.width, height: params.height, }); if (cachedResponse) { console.log("[api/image] got cached image"); return new Response(cachedResponse.buffer.slice(), { headers: { "Content-Type": cachedResponse.mediaType, }, }); } else { console.log("[api/image] no image in cache"); } const remoteImage = await getRemoteImage(imageUrl); if (typeof remoteImage === "string") { console.log("[api/image] ERROR " + remoteImage); return new Response(remoteImage, { status: 400 }); } const modifiedImage = await modifyImage(remoteImage.buffer, { ...params, mode: "resize", }); console.log("[api/image] resized image", { imageUrl }); cache.setImage(modifiedImage, { url: imageUrl, width: params.width, height: params.height, mediaType: remoteImage.mediaType, }); return new Response(modifiedImage, { headers: { "Content-Type": remoteImage.mediaType, }, }); };