Rendering PDF

Turn HTML into paginated PDFs with print layouts, margins, and bleed.

The renderPdf function converts an HTML file or direct HTML string into a fully paginated, print-ready PDF using standard CSS Paged Media rules and Paged.js under the hood.

The host must grant the session's kaya.render.pdf capability to a Paged.js tool binding. Relative VFS assets are transferred through retained artifact custody; file: URLs are rejected because they would grant ambient host filesystem access. Remote resources require an admitted HTTPS origin and make the render an open-world effect, disabling closed-world provider caching. Native deployments configure a paged_pdfs provider in the Rust tool-server manifest with an absolute Chromium program, a private workspace_root, artifact storage and explicit sandbox policy. The provider stages its pinned Paged.js browser bundle and drives Chromium directly. Remote resources require explicit allowed_resource_origins; no Node, Bun, Puppeteer or pagedjs-cli process is required by this provider.

  • Asymmetrical Recto/Verso Margins: Ideal for book binders, leaving larger inner gutters.
  • Bleed & Crop Marks: Extends backgrounds to prevent raw edges, adding exact registration crop lines.
  • Custom Header/Footer Running Elements: Page numbering, chapter titles, and footer marks.

Signature

renderPdf(
  htmlContent?: string,           // HTML content string to render
  inputFile?: string,             // Alternatively, VFS path to source HTML file
  filename: string,               // Output PDF filename (without .pdf extension)
  pageSize?: "A4" | "A5" | "Letter" | "Legal", // Named page size (default: "A4");
                                  //   ignored if customPageSize is set
  customPageSize?: {              // Arbitrary trim size — required for sizes outside
    width: string;                //   the named enum (e.g. a 7x10in book trim)
    height: string;
  },
  orientation?: string,           // Named sizes default to portrait; custom sizes keep order if omitted
  bleed?: string,                 // Bleed amount as a CSS length (e.g. "0.125in")
  addBleed?: boolean,             // [deprecated] alias for bleed: "0.125in"
  addCropMarks?: boolean,         // Inject page crop marks (default: false)
  margins?:                       // Body-page margins: EXACTLY one of two shapes,
    { top: string; bottom: string; left: string; right: string }    // symmetric (digital)
  | { top: string; bottom: string; inner: string; outer: string },  // recto/verso (print)
  mode?: "digital" | "print-interior", // Defaults preset (see below)
  enforceEvenPageCount?: boolean, // Append a blank page when the count is odd
  additionalCss?: string,         // Additional CSS injected into the document
  timeout?: number                // Render timeout in milliseconds (default: 30000)
) -> file

Page size: pageSize only accepts the named enum A4 | A5 | Letter | Legal. For any other trim (e.g. a 7×10in cookbook), pass customPageSize: {width: "7in", height: "10in"} — it wins over pageSize when both are set.

Orientation: Named sizes default to portrait. For a custom size, omitted orientation keeps the authored width/height order. Explicit landscape places the longer side first; portrait places the shorter side first. Already oriented and square sizes stay unchanged. measureHtmlPageFill uses the same page-size parameter policy and A4 default; authored page CSS has a separate policy and can still produce different measured and printed boxes.

Margins: margins is a union of two exact shapes — all four fields of the chosen shape are required:

  • {top, bottom, left, right} — symmetric; for digital/on-screen output with no binding.
  • {top, bottom, inner, outer} — asymmetric recto/verso; inner is the gutter (binding) side, outer the thumb side. Maps to @page :left / :right CSS.

Bleed: Use bleed: "0.125in" (a CSS length). addBleed: true is a deprecated alias for bleed: "0.125in" — it does not touch margins.

Lengths: customPageSize, margins and bleed take absolute CSS lengths: an unsigned number followed by in, cm, mm, pt, pc or px, such as "7in", "178mm" or "0.125in". A page side must be 3pt to 14400pt (200in). Margins and bleed may be zero ("0" is accepted) up to 14400pt. Any other value fails the call before rendering, with an error naming the field and the value. That includes a unitless number, a relative unit such as em or %, and an uninterpolated "{{name}}" marker. Without this check Chromium would drop the invalid @page declaration and print a Letter page. String literals nested in an object are not interpolated, so pass computed sizes as values: customPageSize: { width: coverWidth, height: coverHeight }, not { width: "{{coverWidth}}" }.

Mode presets: mode supplies defaults for the other knobs when you haven't passed them explicitly (anything explicit always wins). "digital" = symmetric margins, no bleed, no page-parity enforcement. "print-interior" = recto/verso margins (wider gutter), 0.125in bleed, enforceEvenPageCount: true. Modes do not pick a trim — provide customPageSize yourself.

Example

let newsletter = "<html><body><h1>Kaya Monthly Digest</h1><p>Welcome to June.</p></body></html>"

let bookPdf = renderPdf(
  htmlContent: newsletter,
  filename: "june-newsletter",
  pageSize: "Letter",
  margins: { top: "0.75in", bottom: "0.75in", inner: "1in", outer: "0.75in" },
  bleed: "0.125in",
  addCropMarks: true
)
// Produces "june-newsletter.pdf" in the VFS and returns its file metadata

// Print-interior preset with a custom trim
save(content: newsletter, filename: "book.html")
let interior = renderPdf(
  inputFile: "book.html",
  filename: "book-interior",
  customPageSize: { width: "7in", height: "10in" },
  mode: "print-interior"
)

Backward Compatibility: htmlToPdf() is supported as an alias for renderPdf().

Host Setup

Configure the renderer and grant the session access before running these examples. See the rendering overview for dependencies and the Tool Server guide for host setup.