Takumi: OG Images and Paged PDFs from JSX and CSS Without a Headless Browser
Render OG images and paged PDFs from JSX, HTML, and CSS. No headless browser. Runs on Node.js, Cloudflare Workers, browsers, and Rust.
At a glance
- What is it?
- Takumi is a Rust rendering engine that turns JSX, HTML and CSS into PNG, WebP, GIF or paged PDF output on Node.js, Bun, Cloudflare Workers, browsers and Rust. It is aimed at teams that generate social cards or invoices at request time and do not want to ship Chromium to do it.
- Who is it for?
- Adopt Takumi if you generate OG images or paged PDFs inside a request path and want to avoid shipping a browser. Do not adopt it if your markup depends on the full CSS surface Chrome implements, or if you need low-level drawing control and would rather keep pdfkit.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What Takumi replaces, and who feels the pain
The project targets a narrow, expensive job: producing an image or a document from markup at runtime, on a server, without a browser process. The README frames the whole thing in one line: "Generate OG images and PDF documents from JSX, HTML, and CSS. No headless browser required."
That sentence is the pitch and the boundary. If you have ever run Puppeteer or Playwright on a serverless function to screenshot a card, you know the cost: a large binary, a cold start, a process to manage, and a container image that grows every time Chrome does. Takumi's answer is to reimplement layout, text shaping, compositing and encoding in Rust (with a WebAssembly build for runtimes that cannot load a native binding), so the render happens in-process.
The audience is therefore specific. Teams generating Open Graph cards from user data, teams generating invoices or reports as paged PDFs, and teams doing either on Cloudflare Workers or in a browser tab where a native binary is not an option. It is not a general HTML-to-PDF service for arbitrary web pages, and the README's own migration table says so when it compares itself to Puppeteer.
The rendering pipeline: one tree, three outputs
The architecture is visible in the repository layout. The Cargo workspace lists crates with distinct jobs: takumi-core, takumi-raster, takumi-svg, takumi-pdf, takumi-paint, takumi-html, plus binding crates takumi-napi and takumi-wasm, and takumi-image-response for the framework integration. The npm side mirrors this with takumi-js, takumi-pdf-js, takumi-helpers and takumi-paint.
The README describes the flow as "One component tree renders as an image, an animation, or a paged PDF." Layout, text shaping, compositing and encoding all happen inside the engine. The workspace dependencies confirm the components: harfrust for shaping, cssparser and selectors for CSS, html5ever and markup5ever_rcdom for HTML parsing, tiny-skia and resvg for raster and SVG, pdf-writer for PDF output, and encoders for png, gif, zune-jpeg and image-webp.
Two design choices stand out. First, the same tree can be sampled across time for animations, which is why @keyframes and animate-spin can become WebP, APNG, GIF or video frames. Second, the PDF path is not a screenshot: text stays selectable and searchable, fonts embed as subsets, and tagged PDF is on by default. That is a different product from printing a page to PDF, and it is the reason the migration guide from Puppeteer warns that you must preload remote assets and that Takumi supports less CSS than Chrome.
Installing takumi-js and rendering a first OG image
The README splits the packages by output type: takumi-js for images and animations, takumi-pdf for paged PDF. The quick start uses Bun to run JSX directly, though the packages are ordinary npm packages.
bun i takumi-js # PNG, JPEG, WebP, SVG, animations
bun i takumi-pdf # paged PDFThe README's image example is a file named image.tsx, run with bun image.tsx, which writes a 1200 x 630 PNG. Note the tw attribute, which applies Tailwind v4 utilities directly, arbitrary values included.
import { render } from "takumi-js";
import { writeFile } from "node:fs/promises";
const image = await render(
<div tw="w-full h-full flex items-center justify-center bg-linear-to-b from-blue-100 to-red-50">
<h1 tw="text-6xl font-bold">Hello from Takumi</h1>
</div>,
{ width: 1200, height: 630 },
);
await writeFile("./output.png", image);What you should see is ./output.png at 1200 by 630 pixels, with the headline centered on a vertical gradient. The render call returns encoded bytes, so writing them is your job.
Fonts are the part most first attempts get wrong. The README's font example uses the googleFonts helper from takumi-js/helpers and passes the result as the fonts option, with variable-font axes set through fontVariationSettings.
import { render } from "takumi-js";
import { googleFonts } from "takumi-js/helpers";
const image = await render(
<div
tw="w-full h-full flex items-center justify-center"
style={{
fontSize: 72,
fontFamily: "Fraunces",
fontVariationSettings: "'opsz' 72, 'wght' 700",
}}
>
Hello from Takumi
</div>,
{
width: 1200,
height: 630,
fonts: googleFonts([{ name: "Fraunces", weight: "100..900", axes: { opsz: "9..144" } }]),
},
);The README is explicit that multilingual text depends on the loaded fonts covering the scripts you emit: Arabic shaping, bidirectional layout, CJK and emoji are supported "when the loaded fonts cover them." For CJK specifically, the lang attribute selects each language's own Han glyphs for the same code points, which matters if you render the same string for Japanese and Chinese audiences.
Paged PDF: where Takumi's model differs most
The PDF path is where the engine makes promises a browser cannot easily keep. Page breaks honor break-before: page, break-after: page and break-inside: avoid. Widows and orphans default to 2. Headers and footers repeat on every page, and the takumi-pdf/primitives module exports PageNumber and TotalPages so a footer can read "Page 1 of 4" without you counting pages yourself.
Tables share column widths across pages and repeat their thead on every page, which is the behavior people actually want from an invoice or a report and rarely get from a naive print stylesheet. Links and metadata carry into the output, and outline: true builds bookmarks from headings. Attachments can embed files, with Factur-X e-invoice XML named as an example, and the examples directory contains an e-invoice example.
The README gives this footer shape for an A4 document. The primitives are imported from a separate subpath, takumi-pdf/primitives, not from the package root.
import { render } from "takumi-pdf";
import { PageNumber, TotalPages } from "takumi-pdf/primitives";
import { writeFile } from "node:fs/promises";
const pdf = await render(
<main>
<h1>Invoice</h1>
<p>Total: $1,250.00</p>
</main>,
{
size: "a4",
footer: (
<div tw="flex w-full justify-center text-[10px] text-gray-500">
Page <PageNumber /> of <TotalPages />
</div>
),
},
);
await writeFile("invoice.pdf", pdf);Tagged PDF being on by default is a notable default: it means accessibility structure is present unless you turn it off, and the README points to a separate PDF/A and PDF/UA guide for conformance options and validation. If your output must conform to an archival or accessibility standard, that guide is the document to read, not the README.
The CSS gap, and where Takumi is the wrong tool
The README's own migration table is the most honest part of the project. Moving from Puppeteer or Playwright for PDFs means replacing page.pdf() with render(), and the table states two constraints in the same cell: "You must preload remote assets, and Takumi supports less CSS than Chrome."
That is the central limitation. Takumi implements a large subset of CSS, and the feature list is long: flexbox, CSS Grid, block, inline and float layout, complex selectors, var(), calc(), media queries, masks, clipping, filters, blend modes, SVG filters through filter: url(...), text on a path via offset-path, background-clip: text, conic gradients, and a corner-shape property that swaps round corners for squircle, bevel, scoop, notch or superellipse(n). But it is not Chrome. If your template relies on a CSS feature outside that subset, you will not get a slightly wrong render; you will get a different render, and the README does not enumerate the unsupported set.
The second constraint is asset loading. Remote images must be preloaded rather than fetched lazily during layout. That is a real change to how you write a template: anything referenced by URL has to be resolved and handed to the renderer first. The README does not document a fallback for a missing font or a failed preload, so treat font coverage and asset resolution as your responsibility.
A third boundary comes from the comparison table itself. Against pdfkit, the advice is to "Keep pdfkit when you need low-level drawing control." Takumi positions lines and boxes through CSS layout, not through explicit coordinates. If your PDF is a chart drawn with vector primitives at computed positions, the CSS model is working against you.
Satori, next/og and the alternatives worth comparing
The closest alternative for the image half is satori, and the README treats it as the primary migration source: replace satori() with renderSvg(), or call render() for encoded image bytes, and it links a comparison page. The practical difference in approach is output and runtime. Satori's model is SVG-shaped; Takumi's render() returns encoded image bytes directly, and the same engine also produces animations and paged PDFs. If all you need is an SVG string for an OG tag, that difference may not matter to you.
For Next.js users, next/og is the other path, and the migration is described as swapping the ImageResponse import while keeping explicit Flexbox styles and comparing the rendered output. That last clause is the honest part: the two renderers will not produce pixel-identical results, so a migration is a visual diff, not a drop-in replacement.
For PDFs, the alternatives split by what you need. Puppeteer and Playwright give you Chrome's full CSS and take a browser process in return. @react-pdf/renderer gives you a component model, and the README's table says replacing Document, View and Text with HTML elements and CSS means browser viewers and some text controls have no equivalent. pdfkit stays the right answer for low-level drawing. Takumi's position is that you already write HTML and CSS, and you want that same tree to become a card, an animation or a document without a second rendering stack.
Licence, release cadence and what upgrading costs
The repository carries both LICENSE-MIT and LICENSE-APACHE at the top level, and the README's badge reads "MIT / Apache-2.0". That dual licence is the standard Rust arrangement: you choose one. Note that the workspace contains multiple published artifacts with their own versions, including takumi-js, takumi-pdf and the takumi crate on crates.io, so a licence question should be checked against the specific package you ship, not only the repository root. This is a description of what the files say, not legal advice.
The release history shows the JS packages and the PDF package versioned separately: [email protected] and [email protected] both landed on 2026-09-15, with [email protected] on 2026-09-06. The PDF package is still on a 0.x line while the image package is at 2.x, which is a reasonable signal about where the API is more settled. The last push to the repository was on 2026-09-23, so the project is current.
Upgrade cost is driven by the native binding. Node.js and Bun load a prebuilt native binary for macOS, Linux (glibc and musl) and Windows on x64 and ARM64, while Cloudflare Workers and browsers load the WebAssembly build. That means a version bump can change both the JS API and the binary underneath it, and a lockfile that resolves a different platform binary is a real failure mode. The repository pins several build-time dependencies through a catalog and overrides block in package.json, including a pinned vite version, which tells you the maintainers treat the toolchain as part of the compatibility surface.
Editorial conclusion
Adopt Takumi if you generate OG images or paged PDFs inside a request path and want to avoid shipping a browser. Do not adopt it if your markup depends on the full CSS surface Chrome implements, or if you need low-level drawing control and would rather keep pdfkit. Before committing, render your real component tree, not a hello-world: check that your fonts cover every script you emit, confirm that any remote image is preloaded, and verify your Cloudflare Workers bundle against the WebAssembly build rather than the native binding.
Frequently asked questions
Does Takumi need a headless browser to render images or PDFs?
No. The README states that it generates OG images and PDF documents from JSX, HTML and CSS with no headless browser required, handling layout, text shaping, compositing and encoding inside a Rust engine.
Which runtimes can run Takumi?
The README lists Node.js and Bun through a prebuilt native binding for macOS, Linux (glibc and musl) and Windows on x64 and ARM64, Cloudflare Workers and browsers through the WebAssembly build, and Rust applications through the takumi crate.
What is the difference between takumi-js and takumi-pdf?
The README says to use takumi-js for images and animations (PNG, JPEG, WebP, SVG) and takumi-pdf for paged PDF documents. They are separate packages with separate version lines.
Can Takumi replace Puppeteer or Playwright for PDF generation?
The migration table says to replace page.pdf() with render(), but it also states that you must preload remote assets and that Takumi supports less CSS than Chrome, so it is a migration with visual differences rather than a drop-in swap.
What licence does Takumi use?
The repository contains LICENSE-MIT and LICENSE-APACHE at the top level, and the README badge reads MIT / Apache-2.0. The workspace publishes several packages, so check the licence of the specific package you ship.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/kane50613-takumi)