Fresh Images
Image manipulation and optimization for Fresh.
Inspired by nuxt/image and next/image.
Demo
A working demo is deployed at https://fresh-images.deno.dev.
Install
Modify the import map in your Fresh project to include ImageScript and the Fresh images plugin.
deno.json
:
{
"imports": {
"fresh_images/": "https://deno.land/x/fresh_images/",
"imagescript/": "https://deno.land/x/imagescript@1.2.15/"
}
}
Include the plugin and desired transformation functions in your
fresh.config.ts
file.
import { defineConfig } from "$fresh/server.ts";
import ImagesPlugin from "fresh_images/mod.ts";
import { resize, rotate } from "fresh_images/transformer.ts";
export default defineConfig({
plugins: [
ImagesPlugin({
route: "/image",
realPath: "./static/images",
transformers: { resize, rotate },
}),
],
});
Initializing the Plugin
realPath
— Defaults to ./static/image
. This is the static asset
directory which contains images to transform.
route
— Defaults to /images
. This property defines the alias URL to the
image directory. Requests to this route are able to receive transformation
function parameters to manipulate images in the realPath
directory.
Note:
route
should not be the same name as a subdirectory in./static
.
tranformers
— An object containing transformation functions, with the
function name (fn
) as keys and the image transformation function as the value.
build
— Optional function to pass to Fresh’s buildStart
hook. Useful for
image preprocessing/pre-rendering.
Note: Build output must be saved in the
./static
directory in order to be served. Use.gitignore
to exclude the output directory if necessary.
Creating Transformations
A transformer function accepts an instance of ImageScript’s Image
or GIF
class to be modified according to the given URL parameters. It must also return
the modified instance of Image
or GIF
.
Example
import ImagesPlugin, {
extendKeyMap,
getParam,
transform,
} from "fresh_images/mod.ts";
import { decode, type GIF, type Image } from "imagescript/mod.ts";
/**
* Custom transformer example.
* Rotate an image hue by a random number of degrees.
* Optionally accept a query parameter to invert the hue.
*/
const myTransformer = async (img: Image | GIF, req: Request) => {
const randomDegrees = Math.floor(Math.random() * 360);
// Use `extendKeyMap` to lookup custom parameters
const invert = getParam(req, "invert", extendKeyMap({ invert: "i" }));
// Use the `transform` helper function to apply asynchronous transformations.
// This ensures transformations will be applied to every frame of a GIF animation.
if (invert) {
img = await transform(img, (frame) => Promise.resolve((frame as Image).invertHue()));
}
return transform(
img,
(frame) => Promise.resolve((frame as Image).hueShift(randomDegrees)),
);
};
export default defineConfig({
plugins: [
ImagesPlugin({
transformers: {
cool: myTransformer,
withCustomRoute: {
// Always applies this `handler` transformation to the route.
// Uses same static image directory!
path: "/desaturate",
handler: (img: Image | GIF) =>
transform(
img,
(img) => Promise.resolve((img as Image).saturation(0, true)),
),
},
},
}),
});
More examples are available in the demo repository.
Tip
Pass multiple instances of the plugin to allow transformations in more than one image directory.
export default defineConfig({
plugins: [
// This creates a route, "/images", that will serve images from the "./static/image" directory.
ImagesPlugin(),
// Create a different route to access another directory.
ImagesPlugin({
route: "/placeholder",
realPath: "./static/placeholders",
transformers: {
// Only specified transformers will be available on this route.
},
}),
],
});
Note: Nested directory routes are currently not supported.
Usage
Pass each transformation to apply as a fn
parameter. Reference the
key map to view valid URL parameter keys.
URL Parameters
Pass image transformation parameters in URL:
<!-- Rotate `./static/images/nyan.gif` 45 degrees and resize it to 500×600px -->
<img src="/img/nyan.gif?fn=rotate&d=45&fn=resize&rw=500&rh=600" />
JSX Component
Returns headless div.fresh-image
. Contains img.fresh-image__placeholder
and/or img.fresh-image__image
.
import FreshImage from "fresh_images/src/components/FreshImage.tsx";
// Example using the `transformations` property
<FreshImage
src="/image/meow.png"
alt="Cropped to 100px by 100px starting at 100px by 100px. Then resized to 200px by 200px."
transformations={[{
fn: "crop",
cropX: "100",
cropY: "100",
cropWidth: "100",
cropHeight: "100",
}, {
fn: "resize",
resizeWidth: "200",
resizeHeight: "200",
}]}
/>
// Define a `placeholder` image to display while the `src` image loads.
<FreshImage
src="/image/cat.jpg"
placeholder="/placeholders/loading.png"
alt="Image with custom transformation and resize"
transformations={[{
fn: "cool",
}, {
fn: "resize",
rw: "400",
}]}
/>
// Set the `preload` property to inject `<link rel="preload">` for this image.
// (Use wisely.)
<FreshImage
src="/image/chonk.jpg"
alt="Big, hidden image"
preload="true"
transformations={{
fn: "resize",
resizeWidth: "1000",
quality: 100
}}
/>
Caching
Deno Deploy currently does not support the Cache API; however, Deno KV can be utilized to serve cached images. Set the following environment variables to enable caching in Deno Deploy, but use with caution. Deno KV is not an ideal blob storage solution and comes with certain costs and limitations.
Environment Variables
FRESH_IMAGES_USE_CACHE
– Enables image caching via Cache API (when available) or Deno KV.FRESH_IMAGES_USE_KV
— WhenFRESH_IMAGES_USE_CACHE
istrue
, setting this variable totrue
will cache images using kv_toolbox
If FRESH_IMAGES_USE_CACHE=true
and FRESH_IMAGES_USE_KV=false
, caching will
not be used on Deno Deploy.
Optional Build Step
Although caching is supported for images rendered on-the-fly, it is always faster to load a pre-rendered, static asset when available.
The Fresh images plugin runs the function provided in the build
property
during
ahead-of-time builds.
This function receives the same arguments passed to the ImagesPlugin
instance.
import type { ImagesPluginOptions } from "fresh_images/src/types.ts";
import ImagesPlugin, { transform } from "fresh_images/mod.ts";
import { ensureDir, join } from "$std/fs/mod.ts";
import { defineConfig } from "$fresh/server.ts";
import { decode, GIF, Image } from "imagescript/mod.ts";
/**
* Pre-optimize images before serving them.
*/
const myBuildFunction: ImagesPluginOptions["build"] = async ({
realPath,
}) => {
const targetDir = realPath ?? "./static";
const files = Deno.readDir("./static/image");
await ensureDir(targetDir);
// Resize all images in the directory
for await (const file of files) {
if (!file.isFile) {
continue;
}
const input = await Deno.readFile(`./static/image/${file.name}`);
const output = await transform(
await decode(input),
(img) => Promise.resolve((img as Image).resize(Image.RESIZE_AUTO, 100)),
);
// Encode at lowest quality
if (output instanceof GIF) {
await Deno.writeFile(
join(targetDir, file.name),
await output.encode(30),
);
continue;
}
await Deno.writeFile(
join(targetDir, file.name),
await output.encodeJPEG(1),
);
}
};
export default defineConfig({
plugins: [
ImagesPlugin({
build: myBuildFunction,
}),
],
});
License
MIT