ImageDefinition
ImageDefinition is a specialized CoValue designed specifically for managing images in Jazz applications. It extends beyond basic file storage by supporting a blurry placeholder, built-in resizing, and progressive loading patterns.
Beyond ImageDefinition, Jazz offers higher-level functions and components that make it easier to use images:
createImage()- function to create anImageDefinitionfrom a fileloadImage,loadImageBySize,highestResAvailable- functions to load and display images
Image- Component to display an image
The Image Upload example demonstrates use of images in Jazz.
Creating Images
The easiest way to create and use images in your Jazz application is with the createImage() function:
The createImage() function:
- Creates an
ImageDefinitionwith the right properties - Optionally generates a small placeholder for immediate display
- Creates multiple resolution variants of your image
- Returns the created
ImageDefinition
Configuration Options
declare function createImage( image: Blob | File | string, options?: { owner?: Group | Account; placeholder?: false | "blur"; maxSize?: number; progressive?: boolean; }, ): Promise<Loaded<typeof ImageDefinition, { original: true }>>;
image
The image to create an ImageDefinition from.
This can be a Blob or a File.
owner
The owner of the ImageDefinition. This is used to control access to the image. See Groups as permission scopes for more information on how to use groups to control access to images.
placeholder
Disabled by default. This option allows you to automatically generate a low resolution preview for use while the image is loading. Currently, only "blur" is a supported.
maxSize
The image generation process includes a maximum size setting that controls the longest side of the image. A built-in resizing feature is applied based on this setting.
progressive
The progressive loading pattern is a technique that allows images to load incrementally, starting with a small version and gradually replacing it with a larger version as it becomes available. This is useful for improving the user experience by showing a placeholder while the image is loading.
Passing progressive: true to createImage() will create internal smaller versions of the image for future uses.
Create multiple resized copies
To create multiple resized copies of an original image for better layout control, you can use the createImage function multiple times with different parameters for each desired size. Here’s an example of how you might implement this:
import { co } from "jazz-tools"; import { createImage } from "jazz-tools/media"; // Jazz Schema const ProductImage = co.map({ image: co.image(), thumbnail: co.image(), }); const mainImage = await createImage(myBlob); const thumbnail = await createImage(myBlob, { maxSize: 100, }); // or, in case of migration, you can use the original stored image. const newThumb = await createImage(mainImage!.original!.toBlob()!, { maxSize: 100, }); const imageSet = ProductImage.create({ image: mainImage, thumbnail, });
Creating images on the server
We provide a createImage function to create images from server side using the same options as the browser version, using the package jazz-tools/media/server. Check the server worker documentation to learn more.
The resize features are based on the sharp library, then it is requested as peer dependency in order to use it.
import fs from "node:fs"; import { createImage } from "jazz-tools/media/server"; const image = fs.readFileSync(new URL("./image.jpg", import.meta.url)); await createImage(image, { // options });
Displaying Images
To use the stored ImageDefinition, there are two ways: declaratively, using the Image component, and imperatively, using the static methods.
The Image component is the best way to let Jazz handle the image loading.
<Image> component
The Image component handles:
- Showing a placeholder while loading, if generated or specified
- Automatically selecting the appropriate resolution, if generated with progressive loading
- Progressive enhancement as higher resolutions become available, if generated with progressive loading
- Determining the correct width/height attributes to avoid layout shifting
- Cleaning up resources when unmounted
The component's props are:
export type ImageProps = Omit< HTMLImgAttributes, "src" | "srcset" | "width" | "height" > & { imageId: string; width?: number | "original"; height?: number | "original"; placeholder?: string; };
Width and Height props
The width and height props are used to control the best resolution to use but also the width and height attributes of the image tag.
Let's say we have an image with a width of 1920px and a height of 1080px.
<Image imageId="123" /> // Image with the highest resolution available <Image imageId="123" width="original" height="original" /> // Image with width 1920 and height 1080 <Image imageId="123" width={600} /> // Better to avoid, as may be rendered with 0 height <Image imageId="123" width={600} height="original" /> // Keeps the aspect ratio (height: 338) <Image imageId="123" width="original" height={600} /> // As above, aspect ratio is maintained, width is 1067 <Image imageId="123" width={600} height={600} /> // Renders as a 600x600 square
If the image was generated with progressive loading, the width and height props will determine the best resolution to use.
Lazy loading
The Image component supports lazy loading with the [same options as the native browser loading attribute].(https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#loading). It will generate the blob url for the image when the browser's viewport reaches the image.
<Image imageId="123" width="original" height="original" loading="lazy" />
Placeholder
You can choose to specify a custom placeholder to display as a fallback while an image is loading in case your image does not have a placeholder generated. A data URL or a URL for a static asset works well here.
Imperative Usage
Like other CoValues, ImageDefinition can be used to load the object.
const image = await ImageDefinition.load("123", { resolve: { original: true, }, }); if (image.$isLoaded) { console.log({ originalSize: image.originalSize, placeholderDataUrl: image.placeholderDataURL, original: image.original, // this FileStream may be not loaded yet }); }
image.original is a FileStream and its content can be read as described in the FileStream documentation.
Since FileStream objects are also CoValues, they must be loaded before use. To simplify loading, if you want to load the binary data saved as Original, you can use the loadImage function.
import { loadImage } from "jazz-tools/media"; const loadedImage = await loadImage(imageDefinitionOrId); if (loadedImage === null) { throw new Error("Image not found"); } const img = document.createElement("img"); img.width = loadedImage.width; img.height = loadedImage.height; img.src = URL.createObjectURL(loadedImage.image.toBlob()!); img.onload = () => URL.revokeObjectURL(img.src);
If the image was generated with progressive loading, and you want to access the best-fit resolution, use loadImageBySize. It will load the image of the best resolution that fits the wanted width and height.
import { loadImageBySize } from "jazz-tools/media"; const imageLoadedBySize = await loadImageBySize(imageDefinitionOrId, 600, 600); // 600x600 if (imageLoadedBySize) { console.log({ width: imageLoadedBySize.width, height: imageLoadedBySize.height, image: imageLoadedBySize.image, }); }
If want to dynamically listen to the loaded resolution that best fits the wanted width and height, you can use the subscribe and the highestResAvailable function.
import { highestResAvailable } from "jazz-tools/media"; const progressiveImage = await ImageDefinition.load(imageId); if (!progressiveImage.$isLoaded) { throw new Error("Image not loaded"); } const img = document.createElement("img"); img.width = 600; img.height = 600; // start with the placeholder if (progressiveImage.placeholderDataURL) { img.src = progressiveImage.placeholderDataURL; } // then listen to the image changes progressiveImage.$jazz.subscribe({}, (image) => { const bestImage = highestResAvailable(image, 600, 600); if (bestImage) { // bestImage is again a FileStream const blob = bestImage.image.toBlob(); if (blob) { const url = URL.createObjectURL(blob); img.src = url; img.onload = () => URL.revokeObjectURL(url); } } });
Custom image manipulation implementations
To manipulate images (like placeholders, resizing, etc.), createImage() uses different implementations depending on the environment.
On the browser, image manipulation is done using the canvas API.
If you want to use a custom implementation, you can use the createImageFactory function in order create your own createImage function and use your preferred image manipulation library.
import { createImageFactory } from "jazz-tools/media"; const customCreateImage = createImageFactory({ createFileStreamFromSource: async (source, owner) => { // ... }, getImageSize: async (image) => { // ... }, getPlaceholderBase64: async (image) => { // ... }, resize: async (image, width, height) => { // ... }, });
Best Practices
- Set image sizes when possible to avoid layout shifts
- Use placeholders (like LQIP - Low Quality Image Placeholders) for instant rendering
- Prioritize loading the resolution appropriate for the current viewport
- Consider device pixel ratio (window.devicePixelRatio) for high-DPI displays
- Always call URL.revokeObjectURL after the image loads to prevent memory leaks