SDK

React PDF

Last updated on

Render a React composition to PDF on the server, and check it against its artifact first.

@paradoc/react-pdf is the Node half of @paradoc/react. It renders a composed document to PDF bytes, checks a composition before a render, and plugs React layers into a form's render() call. The components it renders are the ones in the Components area.

Installation

npm install @paradoc/react-pdf @paradoc/react react react-dom

The default entry installs Takumi, the WebAssembly engine every render uses unless you pick another. Install the optional puppeteer and tailwindcss peers only if you use the Chromium adapter.

EntryPurpose
@paradoc/react-pdfrenderPdf, the adapter types, the Takumi adapter, font and image resources, and React layer renderers
@paradoc/react-pdf/checkcheckComposition and checkElement, which check a composition without producing PDF bytes
@paradoc/react-pdf/chromiumThe experimental Chromium adapter

Render a PDF

renderPdf(element, options?) renders one element, usually an installed Document, Bundle, or Pages tree, and resolves to a PdfRenderResult.

import { writeFile } from "node:fs/promises";
import { renderPdf } from "@paradoc/react-pdf";
import { Document } from "@/components/paradoc/document";
import { Field } from "@/components/paradoc/field";

const { bytes } = await renderPdf(
  <Document artifact={artifact} data={data}>
    <Field path="name" />
  </Document>,
);

await writeFile("invoice.pdf", bytes);

Document and Field are installed components; artifact is a Form and data is its DocumentData. The document's tokens are read off the element itself, so the paper, margin, language, and typography you declared on the root apply to the PDF too.

Options

OptionTypeDescription
planPageBreakPlanThe preview's page plan, so the PDF breaks where the preview did. Without it the engine paginates on its own
furniturePageFurnitureHeader, footer, and stamp drawn on every page. Pass the same object the preview's Pages received
tokensDocumentTokensInputA last token layer for this render only, for example one tenant's accent colour or logo
partialbooleanPrint missing values blank instead of failing. Off by default; use it for drafts, never for a finished document
adapter"takumi" | "chromium" | PdfAdapterThe engine that writes the bytes. Defaults to "takumi"
imagesPdfImage[]Bytes for every image src that is neither a data: URI nor inline SVG markup. No engine fetches images; a missing one fails the render
fontsPdfFontResource[]Application font faces to embed. A preview plan already carries the faces it measured with
applicationCssstringCompiled application CSS (Chromium adapter only)
formatter / progressiveOverride the formatter and its progressive policy
signingMarkersbooleanEmbeds the face that carries seal markers. Set by the seal flow; you rarely set it yourself

The result carries bytes; unknownBreaks and unknownRepeats for plan entries the tree had no keep for; and fontResources, the family, weight, style, and SHA-256 identity of every embedded face.

Use the preview's plan and furniture

The preview's Pages component reports its plan through onPaginate. Send that plan with the render, together with the same furniture, and the PDF matches the preview page for page. The plan also carries the application fonts the preview measured with, and the default adapter embeds them.

import type { ReactElement } from "react";
import { renderPdf, type PageBreakPlan, type PageFurniture } from "@paradoc/react-pdf";
import { PageNumber } from "@/components/paradoc/page-number";

const furniture: PageFurniture = {
  header: <span>Northwind Partners LLP</span>,
  footer: <PageNumber />,
};

async function render(document: ReactElement, plan: PageBreakPlan) {
  return renderPdf(document, { plan, furniture, tokens: { accentColor: "#0f766e" } });
}

A band taller than the margin fails with PageFurnitureOverflowError. Widen the marginPx token or shorten the band. See Page furniture.

Adapters

An adapter is one engine behind renderPdf. Each declares the text directions and furniture slots it was measured to draw, and renderPdf refuses a pairing it cannot make instead of writing a wrong PDF.

AdapterDirectionsFurnitureNotes
takumi (default)ltrheader, footer, stampWebAssembly. Draws from a verified vocabulary of Tailwind classes and embeds application fonts
chromiumltr, rtlheader, footer, stampExperimental. Prints through Chrome, so it matches the browser preview and honours application fonts and CSS

A right-to-left document on Takumi throws UnsupportedDirectionError. applicationCss on Takumi throws UnsupportedApplicationTypographyError. Takumi embeds the plan's captured fonts and ignores the page CSS the plan captured. Classes outside Takumi's vocabulary, or images with no bytes, throw UnsupportedPdfContentError, naming each one.

Chromium

npm install puppeteer tailwindcss
import { renderPdf } from "@paradoc/react-pdf";
import { closeChromium } from "@paradoc/react-pdf/chromium";

const { bytes } = await renderPdf(document, { adapter: "chromium" });
await closeChromium();

adapter: "chromium" loads the adapter on demand and throws MissingAdapterPeerError if a peer is missing. The adapter needs a Chrome: it uses PUPPETEER_EXECUTABLE_PATH when set, then well-known install paths, then Puppeteer's bundled browser (chromiumExecutable() reports which). It keeps one browser open across renders; closeChromium() closes it. Import chromiumAdapter to hold the adapter directly.

Your own engine

Pass any object that implements PdfAdapter.

import { renderPdf, type PdfAdapter } from "@paradoc/react-pdf";

const companyEngine: PdfAdapter = {
  name: "company-engine",
  directions: ["ltr"],
  furniture: ["header", "footer"],
  async render(input, options) {
    // input: element, tokens, plan, furniture, images, fonts, geometry
    // options: lang, dir, signingMarkers
    return { bytes: await printWithCompanyEngine(input, options), unknownBreaks: [], unknownRepeats: [] };
  },
};

await renderPdf(document, { adapter: companyEngine });

An adapter that omits furniture draws none, and a document that declares furniture is refused with UnsupportedFurnitureError.

Check a composition

@paradoc/react-pdf/check walks the same tree a render walks and reports what would fail, without producing a PDF. It is fast enough to run on every save. paradoc check uses it.

import { checkComposition } from "@paradoc/react-pdf/check";

const result = await checkComposition({
  artifact,
  composition: InvoiceDocument,
  data: sample, // optional; defaults to empty fields and parties
});

if (result.unresolvedPaths.length > 0 || result.unsupportedClasses.length > 0) {
  process.exitCode = 1;
}

checkComposition builds the element from composition, artifact, and data. checkElement(element, { adapter? }) checks an element you have already built. Both return a CompositionCheckResult with three lists:

ListContents
unsupportedClassesClasses outside the adapter's verified vocabulary. Always empty for chromium
unresolvedPathsField paths, party roles (party:<role>), party indexes (party:<role>[<index>]), non-picture annexes (image:<path>), and definitions (defs.<name>) that would fail
missingImagesImage src values that are neither data: URIs nor inline SVG markup, which a render needs bytes for

The lists do not combine into a verdict. Whether missingImages fails your check depends on whether you supply image bytes at render time.

React layers

A form artifact can declare a layer with MIME type text/tsx or text/jsx whose path names a composition. reactLayerRenderers(options) returns renderers for both MIME types, so a form's render() call produces a PDF through that layer.

import { p } from "@paradoc/core";
import { reactLayerRenderers } from "@paradoc/react-pdf";

const form = p.form(spec);

const bytes = await form.fill({ fields, parties }).render<Uint8Array>({
  layer: "document",
  renderers: reactLayerRenderers({
    components: { document: InvoiceDocument },
    pdf: { images },
  }),
});

A layer binds through components, keyed by the layer's path or key, or by importing its path from baseDir and taking exportName (default default). Leaving baseDir unset turns the import route off entirely — process.cwd() is never used — so a layer absent from components fails naming both options. paradoc check passes the declaring artifact file's directory as baseDir; paradoc dev loads discovered modules through Vite instead. pdf passes RenderPdfOptions to every render. A layer that cannot be bound throws UnboundReactLayerError. reactRenderer(options) returns the single renderer, and await bindComponent(layer, options) resolves to the bound component without rendering.

Fonts and images

resolveFontResources(resources, options?) resolves PdfFontResource entries (a local path, package specifier, file URL, or HTTP(S) URL, with an optional SHA-256 integrity) and throws FontResourceError on failure. Package specifiers and relative paths resolve from options.resolveFrom, which defaults to process.cwd(); renderPdf exposes the same resolveFrom option. If fonts and the plan's measured fonts name different faces, the render throws FontResourceIdentityMismatchError: repaginate with the current fonts first. A successful result's fontResources lists the family, weight, style, and SHA-256 identity of every embedded face; it does not return font bytes or paths.

Images are passed as { src, data } pairs in images, keyed by the src the tree names.

On this page