You're reading the v2 beta docs. For the stable release, switch to v1 →
TakumiTakumi
Upgrade

Upgrade to v2

Walks through the breaking changes in v2 and how to upgrade from v1.

v2 makes fonts and images explicit per-render resources and tightens rendering to match the web platform. This page covers the breaking changes; for everything else see the releases.

Upgrade with an AI agent

Most v2 breakages surface as type or compile errors, so a coding agent can do the bulk of the work. Bump the packages you use:

npm install takumi-js@beta @takumi-rs/core@beta @takumi-rs/wasm@beta

Then paste the prompt below. Apply only the lines for packages you import, then run the type checker (cargo check for Rust) and fix the rest.

Upgrade this project from Takumi v1 to v2. Apply `old -> new` for the packages it imports.

JS (takumi-js, @takumi-rs/*):
- await the top-level render/renderAnimation/renderSvg and the napi Renderer methods (they await resource loading)
- new Renderer({fonts}) -> new Renderer(); pass fonts per call: render(node, {fonts:[inter]})
- loadFont/loadFonts/loadFontSync -> registerFont (only to reuse a font across renders)
- render option fetchedResources -> images (keyed by src; no global image store)
- format is a string ("png"|"jpeg"|"webp"|...) with optional quality (jpeg/lossy webp) and lossless (webp)
- createImageResponse(opts)(jsx) -> new ImageResponse(jsx, opts)

Rust (takumi crate):
- use takumi::prelude::*; use takumi::render;   (internals now behind the `unstable` feature)
- GlobalContext -> Fonts, passed via RenderOptions::builder().fonts(&fonts)
- Cargo feature "raster" -> "raster-backend" (+ rayon); enable "svg-backend" for render_svg
- OutputFormat::Jpeg/WebP carry Quality; lossless -> WebPLossless; write_image(img, &mut out, fmt)
- measure_layout -> measure, render_sequence_animation -> render_animation; render -> Bitmap
- render_for_layout drops the current_color argument

CSS defaults changed, verify visually: position relative -> static; border/outline width
0 -> medium (3px); negative scale reflects; line-clamp is a shorthand; currentColor in SVG
images is no longer tinted.

The sections below explain each line.

JavaScript

Top-level functions are async

The top-level takumi-js functions render, renderSvg, and renderAnimation await font and image loading before they render, so callers must await them. The napi Renderer methods are async too. The WebAssembly Renderer methods stay synchronous and return bytes directly.

const image = render(node, options); 
const image = await render(node, options); 

measure and encodeFrames are Renderer methods, not top-level exports.

The Renderer constructor takes no arguments

new Renderer(...) no longer accepts fonts or a context. Construct it bare and pass the fonts a render needs through that render's fonts option. The embedded default fonts are decoded once and shared across every renderer.

const renderer = new Renderer({ fonts: [archivo] }); 
const renderer = new Renderer(); 
const image = await renderer.render(node, { fonts: [archivo] }); 

loadFont, loadFonts, and loadFontSync become registerFont

Passing fonts per render covers most cases. To register a font once and reuse it across many renders, a single registerFont replaces the three loaders. It accepts either raw bytes or a { name, data, weight, style } descriptor, and resolves to the families it produced.

renderer.loadFonts([archivo, geist]); 
await renderer.registerFont(archivo); 
await renderer.registerFont(geist); 

Pick the fallback chain with fontFamilies

Each render now takes an ordered fontFamilies list: the family names tried in turn when a glyph is missing. It defaults to every registered family in registration order.

const image = await renderer.render(node, {
  fontFamilies: ["Inter", "Noto Sans JP"], 
});

Renamed and changed options

v1v2Note
fetchedResourcesimagesPre-fetched images, keyed by src.
fonts on renderAnimation / encodeFramesAnimation calls now accept fonts and fontFamilies, matching render.
const image = await renderer.render(node, {
  fetchedResources: resources, 
  images: resources, 
});

The persistent image store and GlobalContext are gone

v1 kept a mutable image store and a GlobalContext on the renderer, so an image registered once stayed available for later renders. v2 removes both. Pass every image the render needs through images, keyed by src. See Load Images.

createImageResponse is removed

Construct ImageResponse directly and pass options inline. For shared defaults, wrap your own helper.

const ogImage = createImageResponse({ fonts: [inter] }); 
export function GET() {
  return ogImage(<OgImage />); 
  return new ImageResponse(<OgImage />, { fonts: [inter] }); 
}

Render to vector SVG

takumi-js now exports renderSvg() alongside render(). It takes the same input (JSX, an HTML string, or a node tree) and the same resource pipeline, but returns an <svg> document instead of a raster bitmap. It is the replacement for satori's SVG output; use render() for PNG or WebP.

import { render, renderSvg } from "takumi-js";

const png = await render(<OgImage />, { width: 1200, height: 630 });
const svg = await renderSvg(<OgImage />, { width: 1200, height: 630 }); 

render, renderSvg, and renderAnimation are all top-level exports, so you can produce a raster image, vector SVG, or animation without constructing a Renderer.

CSS & rendering

Property defaults

These properties changed their default or behavior to match the CSS spec. Verify affected pages visually.

Propertyv1v2Action
positionrelativestaticSet position: relative where you relied on the default containing block or insets.
border-width / outline-width0medium (3px)border: solid red now draws a 3px line; was invisible.
scale (negative)collapses to 0reflectsscale: -1, scaleX(-1) etc. now mirror the element.
line-clampsingle propertyshorthandExpands to max-lines, block-ellipsis, continue; only block-ellipsis inherits.

The sections below cover the subtler cases.

currentColor in SVG images is no longer tinted by the host color

In v1, an SVG supplied as an image (Node::image, background-image: url(...), or mask-image) had its currentColor rewritten to the host element's color before rendering. v2 removes this.

An SVG referenced as an image is an isolated document, so it does not inherit color from the host, the same rule browsers, satori, and @vercel/og follow. Its currentColor now resolves to the SVG's own default (black) instead of the surrounding text color. This also makes the raster and vector (SVG) backends agree, and lets the raster backend reuse its cached parse instead of reparsing the SVG on every render.

currentColor for Takumi's own properties (text color, border-color, box-shadow, outline, text-decoration-color) is unchanged. Only the contents of SVG images are affected.

If you relied on the old behavior to tint an icon, set the color in the SVG markup before passing it in:

const tinted = icon.replace(/currentColor/g, "#3b82f6"); 

position

position now defaults to static, not relative. An element only establishes a containing block for absolutely-positioned descendants, and honors top/right/bottom/left insets, when you opt in. Set position: relative where you relied on the old default.

<div
  style={{
    display: "flex",
    position: "relative", 
  }}
>
  <div style={{ position: "absolute", top: 0, left: 0 }}>Badge</div>
</div>

border-width and outline-width

An omitted width in border / outline now resolves to medium (3px), the CSS initial value, instead of 0. The thin, medium, and thick keywords are accepted. The used width stays 0 when the line's style is none or hidden.

<div style={{ border: "solid red" }}>3px border in v2, invisible in v1</div>

line-clamp

line-clamp is now a shorthand for the max-lines, block-ellipsis, and continue longhands (CSS Overflow 4). block-ellipsis inherits; max-lines and continue do not, so a clamped ancestor no longer forces its line limit onto descendants. -webkit-line-clamp still works and expands to the same longhands.

Rust

takumi is split into focused crates

takumi is now a facade over takumi-core (layout, styling, resources), takumi-raster (the raster backend), and takumi-svg (the vector backend). The facade re-exports a curated stable surface, but the deep module paths it used to expose are gone.

Import the data structures from takumi::prelude and call the entry-point functions from the crate root:

use takumi::{ 
  layout::{node::Node, Viewport, style::{Length::Px, Style, StyleDeclaration}}, 
  resources::font::FontResource, 
  rendering::{render, RenderOptions}, 
  GlobalContext, 
}; 
use takumi::prelude::*; 
use takumi::render; 

Backend internals are no longer re-exported. If you need them, enable the unstable feature and reach them through takumi::unstable; nothing under it is covered by semver.

GlobalContext becomes a Fonts context

With the image store gone, the render context holds only fonts. Build a Fonts, register resources on it, and pass it through RenderOptions::builder().fonts(&fonts).

let mut global = GlobalContext::default(); 
global.font_context.load_and_store(FontResource::new(font_bytes)); 
let mut fonts = Fonts::default(); 
fonts.register(FontResource::new(font_bytes)).unwrap(); 

let options = RenderOptions::builder()
  .viewport(viewport)
  .node(node)
  .global(&global) 
  .fonts(&fonts) 
  .build();

The raster feature is now raster-backend

The default raster backend feature was renamed to mirror svg-backend, and rayon no longer turns it on implicitly. Enable raster-backend (or keep the default features) to render rasters with rayon parallelism.

takumi = { version = "*", default-features = false, features = ["raster", "rayon"] } 
takumi = { version = "*", default-features = false, features = ["raster-backend", "rayon"] } 

Image output quality is modeled per format

OutputFormat::Jpeg and WebP now carry a Quality, lossless WebP moved to its own OutputFormat::WebPLossless variant, and write_image no longer takes a separate quality argument.

write_image(&image, ImageOutputFormat::WebP, 80)?; 
write_image(&image, &mut output, OutputFormat::WebP { quality: Quality::new(80) })?; 

In the napi and wasm bindings, format is a string ("png" | "jpeg" | "webp" | ...) with separate optional quality and lossless fields. quality applies to JPEG and lossy WebP; lossless applies to WebP.

renderer.render(node, { format: "jpeg", quality: 80 });
renderer.render(node, { format: "webp", lossless: true });

Renamed entry points

v1v2Note
measure_layoutmeasureReturns a MeasuredNode.
render_sequence_animationrender_animationMatches the JS binding.
render -> image::RgbaImagerender -> BitmapReach pixels with bitmap.as_raw() / into_raw(), or pass it to write_image.
render_for_layout(..., current_color)render_for_layout(...)The current_color argument is dropped.
let measured = measure_layout(options)?; 
let measured = measure(options)?; 

render_for_layout drops current_color because SVG currentColor is no longer resolved against the host:

image.render_for_layout(width, height, image_rendering, time_ms, current_color)?; 
image.render_for_layout(width, height, image_rendering, time_ms)?; 

More in Changelog!

For the full list of changes, see the releases.

Edit on GitHub

Last updated on

On this page