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 @takumi-rs/core @takumi-rs/wasm

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)
- fonts array now accepts bare URL strings (e.g. fonts: ["https://example.com/Inter.woff2"])
- render option fetchedResources -> images (keyed by src; no global image store)
- render option resourcesOptions -> images.fetch/images.timeout; combine pre-fetched entries and a shared byte cache with the group form images: { sources: [...], fetch: myFetch, fetchCache: myMap }
- extractResourceUrls + fetchResources helpers -> prepareImages({ node, fetchCache? }) from "takumi-js/helpers"
- format is a string ("png"|"jpeg"|"webp"|...) with optional quality (jpeg/lossy webp) and lossless (webp)
- createImageResponse(opts)(jsx) -> new ImageResponse(jsx, opts); import from "@takumi-rs/image-response" directly (the /wasm subpath is removed)
- remove encodeFrames (use renderAnimation or render per frame for raw output)

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; rename feature "svg" to "svg-source" for SVG image input
- 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
- style value types (unstable): LengthDefaultsToZero -> Length, ColorDefaultsToTransparent -> ColorInput
- BackgroundPosition -> PositionValue, BackgroundPositions -> PositionValues; ObjectPosition/TransformOrigin removed -> PositionValue (PositionValue::center() for the 50% 50% default)
- max-width/max-height use MaxSize (initial None); column-gap/row-gap/gap use Gap (initial Normal)
- background shorthand no longer sets background-blend-mode; blend_mode field is gone from Background struct
- font_families and lang options in RenderOptions take resolved FontFamily and Lang instead of raw strings
- write_animation streams animation frames straight to the encoder (e.g. write_animation(options, &mut output, format))
- new takumi-html crate / "from-html" feature for parsing HTML into a Node tree (takumi::from_html)
- core enums (NodeKind, ImageSource, ImageSourceInput, ImageCacheMode, Length, ColorInput), css PropertyId/StyleDeclaration, and css value enums (BlendMode, Filter, BasicShape, ContentValue, TextTransform, WhiteSpaceCollapse, OffsetPath, ImageScalingAlgorithm, Position) are now #[non_exhaustive]: add a wildcard match arm; build via constructors, not struct literals
- css value lists are plain aliases now (Filters = Vec<Filter>, BackgroundImages/Sizes/Repeats and PositionValues = Box<[_]>); drop any newtype wrapper and parse from strings with FromCssStr::from_css_str instead of parse()/from_str

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; transform-origin/object-position default top-left -> center; bolder/lighter
and larger/smaller font keywords now resolve; more elements (lists, sub/sup, ins/del, forms,
details/summary) carry default styles.

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 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

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.
resourcesOptionsimages.fetch / images.timeoutFetch implementation and timeout for remote images.
images.fetchCacheByte cache shared across renders (a Map-like store); dedupes concurrent fetches.
fonts on renderAnimationAnimation 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.

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.
transform-origin / object-positiontop-leftcenter (50% 50%)Set the origin or anchor explicitly where you relied on the top-left default.

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.

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. quality applies to JPEG and lossy WebP; lossless applies to WebP. On the wasm binding WebP is always lossless, so there is no lossless field and quality applies to JPEG only.

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

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.
Node::resource_urls / Style::resource_urlsimage_urlsThe URLs they collect are all images.
ImageResourceErrorImageErrorMatches FontError.
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)?; 

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 gained #[non_exhaustive]: NodeKind, ImageSource, ImageSourceInput, ImageCacheMode, Length, and ColorInput, plus PropertyId, StyleDeclaration, and the spec-tracking CSS value enums (BlendMode, Filter, BasicShape, ContentValue, TextTransform, WhiteSpaceCollapse, OffsetPath, ImageScalingAlgorithm, Position) in takumi-core. 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. Filters and GridTemplateComponents are Vec<_>; BackgroundImages, BackgroundSizes, BackgroundRepeats, and PositionValues are Box<[_]>. Drop the newtype wrapper when you build one. To parse a value from a string, use the FromCssStr trait, 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

max-width and max-height are now represented as a MaxSize enum instead of using Length's auto to mean unbounded. Its initial value is MaxSize::None. column-gap, row-gap, and the gap shorthand are represented as a Gap enum, whose initial value is Gap::Normal (which 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)?; 

More in Changelog!

For the full list of changes, see the releases.

Edit on GitHub

Last updated on

On this page