@unpic/placeholder

ascorbic/unpic-placeholder

This is a library for generating low quality image placeholders (LQIP) by extracting the dominant color from image, or by server-side rendering a BlurHash value. These are displayed while an image is loading, give better appearance and can help reduce the LCP time. It can render the Blurhash to either a set of CSS gradients, or a tiny bitmap image data URI. These are usually around 150 bytes in size, and can be applied as a background image to the img element. It pairs well with unpic-img, a multi-framework image component.

It works on any modern JavaScript runtime, including Deno, Node and WinterCG edge runtimes.

See the demo

Installation

npm install @unpic/placeholder

Dominant Color

The library uses the k-means clustering algorithm to extract a dominant color from the image, and can also generate a palette of colors. This function uses the raw pixel data, so it should be used on the server, with access to the image file. You can use the @unpic/pixels library to get those pixels from a source image. The color can then be stored alongside the image path in the database. The library works in the browser, but using this as a placeholder would defeat the purpose.

Usage

Node:

import { getDominantColor } from "@unpic/placeholder";
import { getPixels } from "@unpic/pixels";
import { promises as fs } from "node:fs";

export async function getDominantColorFromImageFile(filePath: string) {
  // Read the image data from a file
  const pngData = await fs.readFile(filePath);

  // Decode the image data into raw pixel data
  const { data } = getPixels(pngData);

  // Get the dominant color
  return getDominantColor(data);
}

Deno:

import { getDominantColor } from "https://esm.sh/@unpic/placeholder";
import { getPixels } from "https://deno.land/x/get_pixels/mod.ts";

// Load the image from the filesystem
const pngData = await Deno.readFile("image.png");

// Decode the image data into raw pixel data
const { data } = getPixels(pngData);

// Get the dominant color
const color = getDominantColor(data);

BlurHash

A BlurHash is a small string that can be generated from an image and then rendered as a placeholder. It should be pre-generated and stored alongside the image URL in the database.

Unlike other BlurHash libraries, this generates CSS values so it works without client-side JavaScript in any web framework or none, and can be displayed before page hydration. It was designed for unpic-img, which is a multi-framework responsive image component that generates a single <img> tag, but it can be used with <img> tags or with any framework’s image component. It uses no wrapper elements, and is just applied as a style to the <img> tag.

Usage

With <img> tag:

import { blurhashToImageCssString } from "@unpic/placeholder";

const css = blurhashToImageCssString(blurhash);
const img = `<img src=${src} alt=${alt} style=${css} />`;

With unpic-img:

import { blurhashToCssGradientString } from "@unpic/placeholder";
import { Image } from "@unpic/react";

export function MyImage({ src, alt, blurhash }) {
  const placeholder = blurhashToCssGradientString(blurhash);
  return <Image src={src} alt={alt} background={placeholder} />;
}

Generating the BlurHash

You should pre-generate the BlurHash ahead of time. Some CDNs can do this for you. See Imgix for example. This library does not generate the BlurHash. You can use the blurhash library to do this. There are a few ways to do this. In Node:

import { encode } from "blurhash";
import { getPixels } from "@unpic/pixels";

const jpgData = await getPixels(
  "https://res.cloudinary.com/demo/image/upload/c_lfill,w_200,h_100/dog.jpg",
);
const data = Uint8ClampedArray.from(jpgData.data);
const blurhash = encode(data, jpgData.width, jpgData.height, 4, 4);

In Deno:

import { encode } from "https://esm.sh/blurhash";
import { getPixels } from "https://deno.land/x/get_pixels/mod.ts";

const jpgData = await getPixels(
  "https://res.cloudinary.com/demo/image/upload/c_lfill,w_200,h_100/dog.jpg",
);
const data = Uint8ClampedArray.from(jpgData.data);
const blurhash = encode(data, jpgData.width, jpgData.height, 4, 4);

API

getPalette

Gets a palette of colors from an image using k-means clustering, sorted in descending order by dominance.

FunctionType
getPalette(pixels: Uint8ClampedArray, clusterCount?: number) => Colour[]

Parameters:

  • pixels: The RGBA pixel data of the image.
  • clusterCount: The number of colors to return. Defaults to 8.

getDominantColor

Gets the dominant color in an image. Returns an RGB tuple, e.g. [255, 0, 0] for red.

FunctionType
getDominantColor(pixels: Uint8ClampedArray) => Colour

Parameters:

  • pixels: The RGBA pixel data of the image.

kMeansClusters

Performs k-means clustering on an array of pixel data to create a palette of the most common colors.

FunctionType
kMeansClusters(pixels: Uint8ClampedArray, clusterCount: number, sampleSize: number, maxIterations: number) => Cluster[]

Parameters:

  • data: - The RGBA pixel data to cluster.
  • clusterCount: - The number of clusters (i.e., colors) to generate.
  • sampleSize: - The number of pixels to randomly sample from the data. Higher numbers take a long time.
  • maxIterations: - The maximum number of iterations to perform.

rgbColorToCssString

Given a color as an RGB tuple, returns a CSS string e.g. rgb(255, 0, 0)

FunctionType
rgbColorToCssString([red, green, blue]: Colour) => string

blurhashToDataUri

Given a blurhash, returns a data URI of a BMP image. At tiny sizes, this is smaller than a PNG.

FunctionType
blurhashToDataUri(blurhash: string, width?: number, height?: number) => data:image/bmp;base64,${string}“

Parameters:

  • blurhash: the blurhash string
  • width: the width of the generated background image. Keep it tiny. Default is 8 pixels
  • height: the height of the generated background image. Keep it tiny. Default is 8 pixels

blurhashToCssGradients

Given a blurhash, returns an array of CSS linear-gradient() strings. This is a rough approximation of the blurhash image but as pure CSS.

FunctionType
blurhashToCssGradients(blurhash: string, columns?: number, rows?: number) => string[]

Parameters:

  • blurhash: the blurhash string
  • columns: the number of gradients to generate horizontally. Default is 4
  • rows: the number of gradients to generate vertically. Default is 3

blurhashToCssGradientString

Given a blurhash, returns an array of CSS linear-gradient() strings. This is a rough approximation of the blurhash image but as pure CSS.

FunctionType
blurhashToCssGradientString(blurhash: string, columns?: number, rows?: number) => string

Parameters:

  • blurhash: the blurhash string
  • columns: the number of gradients to generate horizontally. Default is 4
  • rows: the number of gradients to generate vertically. Default is 3

blurhashToGradientCssObject

Given a blurhash, returns an object with a CSS background-image property.

FunctionType
blurhashToGradientCssObject(blurhash: string, columns?: number, rows?: number) => CSSObject

Parameters:

  • blurhash: the blurhash string
  • columns: the number of gradients to generate horizontally. Default is 4
  • rows: the number of gradients to generate vertically. Default is 3

blurhashToImageCssObject

Given a blurhash, returns an object with CSS background properties to apply to an img.

FunctionType
blurhashToImageCssObject(blurhash: string, width?: number, height?: number) => CSSObject

Parameters:

  • blurhash: the blurhash string
  • width: the width of the generated background image. Default is 8 pixels
  • height: the height of the generated background image. Default is 8 pixels

blurhashToImageCssString

Given a blurhash, returns a CSS string for a background to apply to an img element.

FunctionType
blurhashToImageCssString(blurhash: string, width?: number, height?: number) => string

Parameters:

  • blurhash: the blurhash string
  • width: the width of the generated background image. Default is 8 pixels
  • height: the height of the generated background image. Default is 8 pixels