Takumi

Images & emoji

Load and cache images, and render emoji.

External images

import {  } from "takumi-js";

const  = < ="https://example.com/image.png" />;

const  = new <string, <>>();

const  = await (, {
  : {
    ,
  },
});

Takumi fetches remote images referenced by src, background-image, mask-image, and list-style-image. An optional fetchCache shares downloaded bytes and pending requests across renders. Use Map<string, Promise<ArrayBuffer>> or an object with the same get, set, and delete contract. Values are promises, not raw buffers.

A plain Map never evicts. If each render uses a different URL, pass a bounded cache such as an LRU, or omit fetchCache.

Fetch limits

import {  } from "takumi-js";

const  = < ="https://example.com/image.png" />;

const  = await (, {
  : {
    : 3000,
    : 4 * 1024 * 1024,
    : () => new (). === "example.com",
  },
});

These options apply to Takumi's image fetches, googleFonts, and fontFromUrl. A custom data loader manages its own requests.

OptionDefaultEffect
timeout30000 msDeadline for each request, including retry delays. 0 or negative disables it.
signalnoneCancel a request and any pending retry.
maxBytes32 MiBReject oversized bodies, using content-length and streamed bytes.
allowUrlallow allReject a URL before fetching it, including redirect destinations.
fetchglobalThis.fetchSupply a custom fetch implementation.

GET and HEAD requests make up to three attempts for connection errors, transient timeouts, or HTTP 408, 429, 500, 502, 503, and 504. Retries wait 100 ms, then 200 ms. A Retry-After header can extend that delay up to one second; a longer value returns the error without retrying. Redirects use the same deadline, with a separate attempt limit per destination.

Caller cancellation and an expired deadline stop retries. Other HTTP errors and failures while reading a response body are not retried. A custom fetch must honor the supplied abort signal.

render() and ImageResponse fetch these URLs from your server. Set allowUrl whenever user input can influence the markup. On a cache miss, it checks each redirect destination, with at most five hops. An image byte-cache hit checks only the entry URL: cached values do not retain redirect history. Do not share images.fetchCache across different URL policies or trust boundaries. DNS is not inspected; use a custom fetch for resolved-address filtering.

Pre-fetched images

Provide images by key so the renderer doesn't have to fetch them itself. Each key can be used in any src field or in the background-image / mask-image CSS properties.

import {  } from "takumi-js/response";

export function () {
  return new (< />, {
    : [
      {
        : "my-logo",
        : () => ("/logo.png").(() => .()),
      },
      {
        : "background",
        : () => ("/background.png").(() => .()),
      },
    ],
  });
}

function () {
  return (
    <
      ={{
        : "url(background)",
      }}
    >
      < ="my-logo" />
    </>
  );
}

Raw pixels

For pixels you compute yourself (charts, generative art), pass raw RGBA instead of encoding a PNG first; nothing is decoded. The Bitmap helper renders a row-major width * height * 4 byte buffer as an <img>:

import {  } from "takumi-js";
import {  } from "takumi-js/helpers/jsx";

const  = new (64 * 64 * 4);

const  = await (< ={64} ={64} ={} />, {
  : 64,
  : 64,
});

Set premultiplied when the bytes are already alpha-premultiplied and the renderer takes them as-is. Outside JSX, an image node's src takes the same { width, height, data, premultiplied? } object.

Decode caching

Takumi caches each decoded image so repeated content is decoded once. Two independent layers exist:

LayerStoresKeyed bySet with
Byte cacheFetched bytesURLimages.fetchCache
Decode cacheDecoded pixelsHash of image bytesper-image cache / images.cache

The decode cache lives in the renderer and is bounded: entries weigh in at their decoded size against a byte budget (16 MiB by default), and once it fills the cache evicts the least recently used. A high-cardinality workload (a different image every render) won't grow memory; it misses the cache and decodes each time. SVG sources and their rasterized pixmaps share the same budget, as do parsed stylesheets.

Set the budget when constructing the renderer; 0 disables caching:

const renderer = new Renderer({ cacheMaxBytes: 64 * 1024 * 1024 });

The per-image cache mode takes two values:

  • auto (default): cache the decoded image within the byte budget.
  • none: read the cache but never populate it. Use it for a one-off image you won't render again, so it can't displace images worth keeping.
import {  } from "takumi-js/response";

export function () {
  return new (< />, {
    : [
      {
        : "banner",
        : () => ("/banner.png").(() => .()),
        : "none", 
      },
    ],
  });
}

To set the mode for every image in a render, including ones Takumi fetches itself, pass images as a group instead of an array. A per-source cache still wins. For a route where each request renders a different image, cache: "none" keeps one-off decodes out of the cache:

import {  } from "takumi-js/response";

export function () {
  return new (< />, {
    : {
      : "none", 
      : [{ : "logo", : () => ("/logo.png").(() => .()) }],
    },
  });
}

function () {
  return < ="logo" />;
}

Decode limits

Takumi rejects oversized images at decode time. The limits guard server renders against decode bombs: small files that declare huge dimensions to force multi-gigabyte allocations.

InputLimit
Image8192×8192 pixels
Animated GIF4× the image budget, summed over all frames

Emoji

Dynamic fetching

ImageResponse accepts a satori-compatible emoji option. The providers are twemoji, blobmoji, noto, openmoji, fluent, and fluentFlat.

import {  } from "takumi-js/response";

export function () {
  return new (< ="flex justify-center items-center text-3xl">Hello 👋😁</>, {
    : "twemoji", 
  });
}

extractEmojis follows the Unicode presentation rules. A pictograph that defaults to text presentation, such as or , stays text and renders with your fonts. Add U+FE0F after it to force the emoji image, or U+FE0E after an emoji to keep the text glyph.

Manual extraction

ImageResponse calls the extractEmojis helper for you. The helper splits emoji from text into image nodes pointing at a CDN. Run the steps yourself to fetch emoji bytes alongside other images.

import {  } from "takumi-js/helpers/emoji";
import {  } from "takumi-js/helpers/jsx";
import {  } from "takumi-js/helpers";
import {  } from "takumi-js/node";

let {  } = await (< ="flex justify-center items-center text-3xl">Hello 👋😁</>);
 = (, "twemoji");

const  = await ({  });

const  = new ();

const  = await .(, {
  ,
});

prepareImages returns image entries for remote URLs missing from sources. It accepts the same fetch cache and limits.

COLR / bitmap fonts

Takumi renders COLR (Color Layer) fonts, the format behind packs like Twemoji-COLR. A COLR file is much smaller than a bitmap emoji font like Noto Color Emoji. The renderer draws emoji from the loaded glyphs, with no network request.

Two steps:

  • Register the font through fonts.
  • Set emoji: "from-font".

Variation selectors override font order. A codepoint followed by U+FE0F renders as a color emoji even when a text font appears earlier in the stack. A codepoint followed by U+FE0E renders as a monochrome glyph even when a color font appears first. Bare codepoints follow stack order, as in browsers.

Last updated on

On this page