Upgrade to v2
Upgrade Takumi from v1 to v2 and migrate JavaScript APIs, Rust imports, resources, and CSS defaults.
v2 makes fonts and images per-render resources and changes several CSS defaults. This guide covers migration from v1, including behavior that later v2 releases restored. For individual versions, see the release notes.
Install
Update the packages your project imports. For the high-level JavaScript API:
npm install takumi-jsIf you use @takumi-rs/core or @takumi-rs/wasm directly, update those packages too. Work through the JavaScript or Rust sections below, then run your type checker and compare rendered output against v1.
JavaScript
Top-level functions are async
The top-level takumi-js functions render, renderSvg, and renderAnimation load fonts and images before they render, so they return promises. await them. The napi and WebAssembly Renderer methods are async too.
const image = render(node, options);
const image = await render(node, options); measure is a Renderer method, not a top-level export. encodeFrames is removed; use renderAnimation (or render per frame for raw output).
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
Pass fonts with each render for most uses. Use registerFont to preload fonts on a renderer you reuse.
renderer.loadFonts([archivo, geist]);
renderer.render(node, { fonts: [archivo, geist] }); To preload a font once and reuse it across many renders, call registerFont, which replaces all three loaders. It takes raw bytes or a { name, data, weight, style } descriptor, and resolves to the families it registered.
renderer.loadFonts([archivo, geist]);
await renderer.registerFont(archivo);
await renderer.registerFont(geist); Pick the fallback chain with fontFamilies
Each render takes an ordered fontFamilies list of 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
| v1 | v2 | Note |
|---|---|---|
fetchedResources | images | Pre-fetched images, keyed by src. |
resourcesOptions | images.fetch / images.timeout | Fetch implementation and timeout for remote images. |
| — | images.fetchCache | Byte cache shared across renders (a Map-like store); dedupes concurrent fetches. |
| — | fonts on renderAnimation | Animation calls now accept fonts and fontFamilies, matching render. |
const image = await renderer.render(node, {
fetchedResources: resources,
images: resources,
});images also takes a group form that holds pre-fetched entries, fetch behavior, and a shared byte cache in one place:
const imageCache = new Map<string, Promise<ArrayBuffer>>();
const image = await render(node, {
resourcesOptions: { fetch: myFetch },
images: {
sources, // pre-fetched entries, not re-fetched
fetch: myFetch,
fetchCache: imageCache, // reused across renders, dedupes concurrent fetches
},
});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() next to render(). It takes the same input (JSX, an HTML string, or a node tree) and the same resources, but returns an <svg> document instead of a raster image. It replaces 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 a raster image, vector SVG, or animation needs no Renderer.
Bare URL strings in fonts
A fonts entry can now be a bare URL string instead of a font descriptor or raw bytes. The font is fetched on demand and cached.
const image = await render(<OgImage />, {
fonts: ["https://example.com/Inter.woff2"],
});@takumi-rs/image-response/wasm export is removed
Import ImageResponse from @takumi-rs/image-response instead of its /wasm subpath:
import { ImageResponse } from "@takumi-rs/image-response/wasm";
import { ImageResponse } from "@takumi-rs/image-response"; Animation frame rate cap
Animations with high frame rates could stall or play incorrectly. In v2, renderAnimation throws when the frame rate exceeds the format's limit:
| Format | Limit |
|---|---|
| WebP, APNG | 90 fps |
| GIF | 50 fps |
CSS & rendering
Property defaults
These properties changed their default or behavior to match the CSS specification. Check affected pages visually.
| Property | v1 | v2 | Action |
|---|---|---|---|
position | relative | static | Set position: relative where you relied on the default containing block or insets. |
border-width / outline-width | 0 | medium (3px) | border: solid red now draws a 3px line; was invisible. |
scale (negative) | collapses to 0 | reflects | scale: -1, scaleX(-1) etc. now mirror the element. |
line-clamp | single property | shorthand | Expands to max-lines, block-ellipsis, continue; only block-ellipsis inherits. |
transform-origin / object-position | top-left | center (50% 50%) | Set the origin or anchor explicitly where you relied on the top-left default. |
The sections below cover the subtler cases.
v2.0–v2.5: currentColor in SVG images ignores the host color
In v2.0 through v2.5, SVG image content uses its own color instead of inheriting the host element's color.
Since v2.6, the host color is the fallback again. currentColor resolves to an explicit color inside the SVG first, then the host color, then black. If you upgrade straight to v2.6 or later, keep icons that use currentColor as they are.
This change does not affect Takumi's own text, borders, shadows, or decorations.
position
position now defaults to static, not relative. An element establishes a containing block for absolutely positioned descendants, and honors top/right/bottom/left insets, only 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.
Wider default element styles
v2 ships a user-agent stylesheet that matches Chromium's. Two changes affect existing markup:
- Relative keywords resolve. v1 ignored
font-weight: bolder/lighterandfont-size: larger/smaller. v2 applies them. - More elements have default styles: lists,
sub,sup,ins,del, form controls,details,summary, andsearch. They render differently than in v1. Override the defaults where you need the old look.
background shorthand drops background-blend-mode
Unlike browsers, the v1 background shorthand parsed a blend-mode token and reset background-blend-mode. In v2, the shorthand neither parses nor resets the blend mode. Set it with the background-blend-mode longhand.
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 stable subset. The deep module paths it used to expose are gone.
Import data structures from takumi::prelude, and call 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. To reach them, enable the unstable feature and import from 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))?;
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 is renamed to mirror svg-backend, and rayon no longer turns it on. To render rasters with rayon parallelism, enable raster-backend or keep the default features.
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::JpegandWebPnow carry aQuality.- Lossless WebP moved to its own
OutputFormat::WebPLosslessvariant. write_imageno 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 binding, format is a string ("png" | "jpeg" | "webp" | ...) with separate optional quality and lossless fields. Field support differs by binding:
| Binding | quality | lossless |
|---|---|---|
| napi | JPEG and lossy WebP | WebP |
| wasm | JPEG only | absent (WebP is always lossless) |
renderer.render(node, { format: "jpeg", quality: 80 });
renderer.render(node, { format: "webp", lossless: true }); // napi onlyRenamed entry points
| v1 | v2 | Note |
|---|---|---|
measure_layout | measure | Returns a MeasuredNode. |
render_sequence_animation | render_animation | Matches the JS binding. |
render -> image::RgbaImage | render -> Bitmap | Reach pixels with bitmap.as_raw() / into_raw(), or pass it to write_image. |
Node::resource_urls / Style::resource_urls | image_urls | The URLs they collect are all images. |
ImageResourceError | ImageError | Matches FontError. |
let measured = measure_layout(options)?;
let measured = measure(options)?; render_for_layout removed its current_color argument in v2.0 and restored it in v2.6. Keep the v1 call shape when upgrading to v2.6 or later.
Length and ColorInput drop their default-flavor type parameter
Length and ColorInput were generic over a const bool, so one enum could default two ways (auto or 0, currentColor or transparent). Both are now plain enums. The LengthDefaultsToZero and ColorDefaultsToTransparent aliases are removed, so use Length and ColorInput. Rendering is unchanged, because each field now declares its zero or transparent initial value.
let inset: LengthDefaultsToZero = Length::Px(0.0);
let inset: Length = Length::Px(0.0); BackgroundPosition is renamed to PositionValue
The CSS <position> value backs background-position, object-position, transform-origin, mask-position, and gradient centers. So BackgroundPosition is renamed to PositionValue, and BackgroundPositions to PositionValues.
The ObjectPosition and TransformOrigin aliases are removed. They aliased BackgroundPosition, so their Default was top-left instead of the 50% 50% those properties default to. Use PositionValue, and PositionValue::center() for the center center initial value.
let origin: TransformOrigin = TransformOrigin::default();
let origin: PositionValue = PositionValue::center(); Core enums are #[non_exhaustive]
Several public enums in takumi-core gained #[non_exhaustive]:
- Core:
NodeKind,ImageSource,ImageSourceInput,ImageCacheMode,Length,ColorInput - Style:
PropertyId,StyleDeclaration - Spec-tracking CSS values:
BlendMode,Filter,BasicShape,ContentValue,TextTransform,WhiteSpaceCollapse,OffsetPath,ImageScalingAlgorithm,Position
Outside the crate, you can no longer build these variants with a struct literal, and a match on them needs a wildcard arm.
match node.kind {
NodeKind::Container(_) => {}
NodeKind::Text(_) => {}
_ => {}
}CSS value lists are type aliases
The multi-value CSS lists are plain aliases, not newtypes. Drop the newtype wrapper when you build one.
| Underlying type | Aliases |
|---|---|
Vec<_> | Filters, GridTemplateComponents |
Box<[_]> | BackgroundImages, BackgroundSizes, BackgroundRepeats, PositionValues |
Parse strings with FromCssStr, which returns an owned ParseError and keeps cssparser out of the public API.
let filters = Filters(vec![Filter::Blur(Length::Px(4.0))]);
let filters = vec![Filter::Blur(Length::Px(4.0))];
let sizes = BackgroundSizes::from_css_str("cover")?;max-width / max-height and gap representation
Two properties moved off Length onto dedicated enums:
| Property | Enum | Initial |
|---|---|---|
max-width, max-height | MaxSize | MaxSize::None (unbounded) |
column-gap, row-gap, gap | Gap | Gap::Normal (computes to 0) |
let max_width = Length::Auto;
let max_width = MaxSize::None;
let gap = Length::Px(0.0);
let gap = Gap::Normal; font_families and lang option types are resolved
These RenderOptions fields now take parsed types instead of raw strings:
| Option | Type | Parse with |
|---|---|---|
font_families | FontFamily | FontFamily::from_css_str |
lang | Lang | Lang::parse |
let options = RenderOptions::builder()
.font_families(Some(vec!["Inter".to_string()]))
.lang(Some("en-US".to_string()))
.font_families(Some(FontFamily::from_css_str("Inter")?))
.lang(Some(Lang::parse("en-US")?)) svg Cargo feature renamed to svg-source
The Cargo feature for SVG image input is renamed to svg-source, to tell it apart from svg-backend, which renders SVG output.
takumi = { version = "*", features = ["svg"] }
takumi = { version = "*", features = ["svg-source"] } Stream animation frames with write_animation
write_animation streams frames straight to the encoder instead of holding the whole sequence in memory, so memory stays bounded. The napi and WASM bindings use it too.
// Legacy eager rendering of animation frames:
let frames = render_animation(options)?;
write_animated_webp(&frames, &mut output, fps)?;
// Modern streaming:
write_animation(options, &mut output, format)?; Parsing HTML with takumi-html
The new takumi-html crate parses HTML and Tailwind markup into a node tree. In the takumi crate, enable the from-html feature to use it:
use takumi::from_html;
let node = from_html(html_source, options)?; Full release notes
For the full list of changes, see the releases.
Last updated on