Takumi

Images & emoji

Load remote images, configure fetch limits and caches, and render emoji with Takumi.

External images

render() and ImageResponse fetch every remote http:// or https:// URL the tree references:

  • an <img> src
  • background-image, mask-image, and list-style-image in a node's style or tw

Takumi does not fetch URLs inside css or a <style> tag. Pass those images as pre-fetched images.

Share a fetchCache across renders to reuse downloaded bytes and pending requests:

Share downloaded image bytes
import {  } from "takumi-js";

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

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

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

fetchCache takes a Map<string, Promise<ArrayBuffer>> or any object with the same get, set, and delete methods. Values are promises, not raw buffers.

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

Fetch limits

Limit image fetches
import {  } from "takumi-js";

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

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

images, googleFonts, and fontFromUrl take these options. A custom data loader makes its own requests, so they do not apply to it.

OptionDefaultEffect
timeout30000 msDeadline for each request, including retry delays. 0 or negative disables it.
signalnoneCancels a request and any pending retry.
maxBytes32 MiBRejects larger bodies, by content-length and by streamed bytes.
allowUrlallow allRejects a URL before fetching it, redirect destinations included.
fetchglobalThis.fetchReplaces the fetch implementation.

Retries

GET and HEAD requests make up to three attempts. They retry when fetch throws a TypeError (such as a connection error) or a TimeoutError, and on HTTP 408, 429, 500, 502, 503, and 504. The waits are 100 ms, then 200 ms. A Retry-After header can extend a wait up to one second. A longer Retry-After returns the error without retrying.

These stop retries:

  • the caller's signal aborts
  • the deadline passes
  • any other HTTP error
  • a failure while reading the response body

A custom fetch must honor the abort signal it receives.

URLs from user input

Takumi fetches these URLs from your server. Set allowUrl whenever user input can reach the markup.

  • allowUrl checks each redirect destination, up to five hops. Each hop shares the same deadline and gets its own attempts.
  • A fetchCache hit checks only the requested URL, because cached bytes keep no redirect history. Do not share one fetchCache across different URL policies.
  • allowUrl sees URLs, not IP addresses. To filter by resolved address, pass a custom fetch.

Pre-fetched images

Give images a key so the renderer never fetches them. Use the key in any src or CSS url().

Load named image sources
import {  } from "takumi-js/response";

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

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

data is raw bytes or a function that returns them. Takumi skips fetching any remote URL that already has an entry.

Raw pixels

Pass raw RGBA pixels instead of encoding a PNG first; Takumi skips decoding. The Bitmap helper renders a row-major buffer of width * height * 4 bytes as an <img>. This example fills a 64 × 64 image with opaque blue:

Render raw RGBA pixels
import {  } from "takumi-js";
import {  } from "takumi-js/helpers/jsx";

const  = new (64 * 64 * 4);

for (let  = 0;  < .;  += 4) {
  .([59, 130, 246, 255], );
}

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

Set premultiplied when the bytes already have premultiplied alpha. Takumi then uses them as they are. 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 decodes once. There are two caches:

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

A data: URI in an <img> or a CSS url() uses the same decode cache, keyed by the bytes it holds. A template that inlines its logo decodes it once. data: URIs always cache in auto mode, because images.cache applies only to sources and to URLs Takumi fetches.

The decode cache belongs to the renderer and has a byte budget, 64 MiB by default. Each entry counts its decoded size. When the cache is full, it evicts the least recently used entry. SVG sources, their rasterized pixels, and parsed stylesheets share the same budget. A workload with a different image every render does not grow memory. It misses the cache and decodes each time.

Set the budget when you create the renderer. 0 turns caching off.

Set the decode cache budget
import {  } from "takumi-js/node";

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

The per-image cache mode takes two values:

  • auto (default): cache the decoded image within the budget.
  • none: read the cache but never add to it. Use it for a one-off image, so it does not push out images worth keeping.
Skip caching one image
import {  } from "takumi-js/response";

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

To set the mode for every image in a render, pass images as an object instead of an array. This includes images Takumi fetches itself. A source's own cache still wins. For a route that renders a different image on every request, cache: "none" keeps one-off decodes out of the cache:

Skip caching all images
import {  } from "takumi-js/response";

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

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

Decode limits

Takumi rejects oversized images when it decodes them. The limits protect servers from decode bombs: small files that declare huge dimensions to force huge allocations.

InputLimit
Image8192 × 8192 pixels
Animated GIF, APNG, or WebP4 × 8192 × 8192 pixels summed over all frames. Frames past 1024 are dropped.

Emoji

takumi-js turns emoji into images by default. It splits each emoji out of the text and fetches its SVG from a CDN, using Twemoji unless you pick another set.

Dynamic fetching

Set emoji to choose the set. The options match satori: twemoji (default), blobmoji, noto, openmoji, fluent, and fluentFlat. render, renderSvg, renderAnimation, measure, and ImageResponse all take it.

Fetch emoji images
import {  } from "takumi-js/response";

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

Emoji detection 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. Add U+FE0E after an emoji to keep the text glyph.

Manual extraction

The extractEmojis helper does the splitting. It replaces each emoji with an image node that points at the CDN. Call it yourself to fetch emoji with your other images on your own renderer.

Extract emoji before rendering
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 fetches the remote URLs missing from sources. It returns the supplied sources and the fetched entries together. It takes the same fetch cache and limits.

COLR / bitmap fonts

Takumi draws emoji from color fonts: COLR fonts such as Twemoji-COLR, and bitmap fonts such as Noto Color Emoji. A COLR file is much smaller than a bitmap font. The glyphs come from the loaded font, so no emoji request goes out.

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

Variation selectors take priority over font order. A code point followed by U+FE0F renders as a color emoji, even when a text font comes first in the stack. A code point followed by U+FE0E renders as a monochrome glyph, even when a color font comes first. Bare code points follow stack order, as in browsers.

Last updated on