Upgrade to v2
Upgrade Takumi from v1 to v2 and migrate JavaScript APIs, Rust imports, resources, and CSS defaults.
v2 makes fonts and images explicit per-render resources and changes several CSS defaults. This guide covers migration from v1, including changes restored in later v2 releases. See the release notes for individual versions.
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. Follow the JavaScript or Rust sections below, then run your type checker and compare rendered output.
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 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, 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
| 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() 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.
Bare URL strings in fonts
fonts entries can now be bare URL strings instead of loaded font descriptors or raw bytes. The font is fetched on demand and cached automatically.
const image = await render(<OgImage />, {
fonts: ["https://example.com/Inter.woff2"],
});@takumi-rs/image-response/wasm export is removed
If you were importing ImageResponse from the @takumi-rs/image-response/wasm subpath, you must now import it from @takumi-rs/image-response directly:
import { ImageResponse } from "@takumi-rs/image-response/wasm";
import { ImageResponse } from "@takumi-rs/image-response"; Animation frame rate cap
High frame rates in animations could stall or fail to play correctly. In v2, renderAnimation throws an error if the frame rate exceeds the format's limit: 90 fps for WebP and APNG, and 50 fps for GIF.
CSS & rendering
Property defaults
These properties changed their default or behavior to match the CSS spec. Verify 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
This change applies to v2.0–v2.5. In those versions, 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. An explicit color inside the SVG takes precedence, followed by the host color, then black. When upgrading directly to v2.6 or later, keep icons that use currentColor as they are.
Takumi's own text, borders, shadows, and decorations are unaffected by this change.
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.
Wider default element styles
v2 ships a Chromium-parity user-agent stylesheet. Two changes affect existing markup.
Relative keywords now resolve. font-weight: bolder / lighter and font-size: larger / smaller were ignored in v1. They take effect in v2.
More elements carry default styles: lists, sub, sup, ins, del, form controls, details, summary, and search. These elements render differently than 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 no longer parses or resets the blend mode; configure it through the longhand property.
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))?;
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 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 vs 0, currentColor vs transparent). That parameter is gone: both are plain enums. The LengthDefaultsToZero and ColorDefaultsToTransparent aliases are removed; use Length and ColorInput. Rendering is unchanged: the zero/transparent initial values are now declared per field.
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 was renamed to PositionValue, and BackgroundPositions to PositionValues. The ObjectPosition and TransformOrigin aliases are removed; they were aliases of BackgroundPosition, so their Default was top-left, not the 50% 50% those properties actually 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
You can no longer build these variants with a struct literal from outside the crate, 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 instead of 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
Options on RenderOptions now take resolved types instead of raw strings. font_families accepts FontFamily (which can be parsed with FontFamily::from_css_str), and lang accepts Lang (which can be parsed with 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
To distinguish it from the svg-backend feature (used for SVG render output), the cargo feature gating SVG image input is renamed to svg-source.
takumi = { version = "*", features = ["svg"] }
takumi = { version = "*", features = ["svg-source"] } Stream animation frames with write_animation
write_animation now streams animation frames straight to the encoder to keep memory bounded, rather than holding the entire frame sequence in memory. The napi and WASM bindings also use this behind the scenes.
// 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
A new crate takumi-html is introduced to parse HTML and Tailwind markup into a node tree. Under the umbrella takumi crate, this is available via the from-html feature:
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