Images & emoji
Load and cache images, and render emoji.
External Images
Takumi fetches external images in src attributes and CSS background-image / mask-image.
To reuse fetched bytes across renders, pass a fetchCache: a Map<string, Promise<ArrayBuffer>>, or anything with Map-like get/set/delete. It stores the in-flight promise, so concurrent renders needing the same URL share one fetch:
import { } from "takumi-js";
const = < ="https://example.com/image.png" />;
const = new <string, <ArrayBuffer>>();
const = await (, {
: {
,
},
});A plain Map never evicts. If your URLs are high-cardinality (a different image per render), pass a bounded cache such as an LRU, or omit fetchCache.
Fetch limits
Every remote fetch (images, fonts, Google Fonts CSS) is bounded. The group form of images takes the same options:
| Option | Default | Use |
|---|---|---|
timeout | 5000 (ms) | Abort a hanging host. 0 or negative disables it. |
maxBytes | 32 MiB | Reject a body past this size, by content-length or streamed. |
allowUrl | allow all | Return false to block a URL, e.g. an SSRF (server-side request forgery) allowlist. |
fetch | globalThis.fetch | Custom fetch implementation. |
import { } from "takumi-js";
const = < ="https://example.com/image.png" />;
const = await (, {
: {
: 3000,
: 4 * 1024 * 1024,
: () => new (). === "example.com",
},
});render() and ImageResponse fetch these URLs from your server, so set an allowUrl allowlist whenever user input can influence the markup. The policy is checked on every redirect hop (capped at 5), so an allowed URL can't 3xx its way to an internal address. DNS is not inspected; keep a custom fetch for a stricter policy such as 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:
| Layer | Stores | Keyed by | Set with |
|---|---|---|---|
| Byte cache | Fetched bytes | URL | images.fetchCache |
| Decode cache | Decoded pixels | Hash of image bytes | per-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.
| Input | Limit |
|---|---|
| Image | 8192×8192 pixels |
| Animated GIF | 4× 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",
});
}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 walks the node tree for remote <img>, backgroundImage, and maskImage URLs, fetches the ones missing from sources, and returns images entries. Pass a fetchCache (a Map<string, Promise<ArrayBuffer>>, or anything with Map-like get/set/delete) to deduplicate concurrent fetches and reuse bytes across renders.
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".
Last updated on