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.
Print Layout Features
- 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)
) -> filePage 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;inneris the gutter (binding) side,outerthe thumb side. Maps to@page :left/:rightCSS.
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().
Related Renderers
- Rendering HTML — create the HTML from React components
- typst() — typeset a PDF from Typst markup
- pdfToImage() — preview PDF pages as a PNG using Poppler
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.