Takumi

From Puppeteer

Replace headless Chrome with a 1.5 MB wasm module.

takumi-pdf was designed against Puppeteer's page.pdf(). Most options have a direct counterpart, header and footer bands use the same class hooks, and pagination follows Chromium's fragmentation rules. Names and input types differ in places, so read the map below.

What changes is the deploy. No browser process, no Chrome install, no page to navigate. The renderer takes a tree and returns bytes, so it runs on Cloudflare Workers.

Before and after

// Puppeteer
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle0" });

const pdf = await page.pdf({
  format: "A4",
  margin: { top: "48px", bottom: "64px", left: "48px", right: "48px" },
  displayHeaderFooter: true,
  footerTemplate: `<div style="font-size:10px;width:100%;text-align:center">
    Page <span class="pageNumber"></span> of <span class="totalPages"></span>
  </div>`,
  printBackground: true,
});

await browser.close();
// takumi-pdf
import {  } from "@takumi-rs/helpers";
import {  } from "@takumi-rs/helpers/html";
import {  } from "takumi-pdf";

const { ,  } = ();

const  = await (, {
  : "a4",
  : { : 48, : 64, : 48, : 48 },
  ,
  : (
    < ="flex w-full justify-center text-[10px]">
      Page < ="pageNumber" /> of < ="totalPages" />
    </>
  ),
  : await (["Inter"]),
});

fromHtml converts the markup already being fed to setContent. JSX and node trees skip that step. Bands take the same input, so a header can be a component instead of a template string.

Option map

page.pdf()takumi-pdf
format: "A4"size: "a4"
width, heightsize: { width, height }
landscapelandscape
marginmargin, in CSS px
displayHeaderFooter + headerTemplateheader
footerTemplatefooter
pageNumber, totalPages classesthe same classes
date, title, url classesinterpolate the value yourself
tagged (default on)tagged (default on)
outlineoutline
printBackgroundalways on
pathwrite the returned Uint8Array
timeout, waitForFontsnot needed: no page to load
scalenot supported: scale the CSS instead
pageRangesnot supported
preferCSSPageSize, @pagenot supported: set size and margin
omitBackgroundnot supported

Margins take numbers in CSS px. Chromium's "0.5in" strings have no equivalent; 48 is that half inch.

Fetching moves to your code

Nothing is fetched implicitly. A browser resolved image URLs and web fonts while it loaded the page. Now the fetch is a call you make:

import {  } from "@takumi-rs/helpers";
import {  } from "@takumi-rs/helpers/html";
import {  } from "takumi-pdf";

const { ,  } = ();
const  = await ({  });

const  = await (, { ,  });

prepareImages walks the tree, fetches every remote <img>, background-image, and mask-image URL, and returns the images entries. It takes a fetchCache to reuse bytes, an allowUrl predicate, and a timeout.

Fonts come from the fonts option rather than a CSS @font-face rule. See Fonts & images.

A render never reaches for a URL on its own. Timeouts and allow-lists sit in the fetch, where they can be enforced.

What Chrome still does better

Chrome is a browser. Takumi is a layout engine with a document-shaped CSS subset.

  • No scripts. Charts and diagrams have to arrive as markup, SVG, or images. A client-side chart library will not run.
  • Narrower CSS. No filter: blur(), drop-shadow(), or backdrop-filter in PDF output. A blurred box-shadow approximates with bands. Grid and flex, transforms, gradients, masks, and clip paths all work.
  • No @page. Page geometry lives in the options.

Keep Puppeteer to reproduce a live web page as it appears in a browser. Move to takumi-pdf for documents written for print, and see Comparison for the measured numbers.

Last updated on

On this page