Documentation

Data & assets

Your data

A document is a component, so your data arrives as props. map, conditions, your own components, context - anything that produces a tree produces a PDF.

import { Divider, Document, Page, Row, Text } from "@jasy/react";

type Line = { id: number; name: string; price: number };

export function Statement({ lines }: { lines: Line[] }) {
  const total = lines.reduce((sum, l) => sum + l.price, 0);
  return (
    <Document size={11}>
      <Page size="A4" margin={48} gap={8}>
        {lines.map((line) => (
          <Row key={line.id} justify="between">
            <Text>{line.name}</Text>
            <Text>{line.price} EUR</Text>
          </Row>
        ))}
        <Divider />
        <Row justify="between">
          <Text bold>Total</Text>
          <Text bold>{total} EUR</Text>
        </Row>
      </Page>
    </Document>
  );
}

A PDF is rendered once, not kept alive like a page: load your data before renderToPdf and pass it in. An effect that sets state after the first render (useEffect fetching data) comes too late - the document has been read by then.

Custom fonts

The standard PDF fonts (Helvetica, Times, Courier) need nothing. To use your own, register the font on <Document fonts> under a name, then use that name as a font. In the browser, fetch the bytes first:

const inter = new Uint8Array(await (await fetch("/fonts/Inter.woff2")).arrayBuffer());
const bytes = await renderToPdf(<Report font={inter} />);

function Report({ font }: { font: Uint8Array }) {
  return (
    <Document fonts={{ Inter: font }} font="Inter">
      <Page size="A4" margin={48}>
        <Text size={24} bold>
          Set in Inter
        </Text>
      </Page>
    </Document>
  );
}

The container does not matter: .ttf, .woff and .woff2 are the same thing here - jasy unwraps the wrapper itself. The font is subsetted on the way into the PDF, so the file stays small. For weights, register one file per style: fonts={{ Inter: { normal, bold, italic } }}.

On a server a plain path works too - fonts={{ Inter: "./fonts/Inter.woff2" }} is the whole story. In the browser there is no filesystem to point at, so it is bytes.

Images

<Image src> takes the image bytes as a Uint8Array, a URL, or - on a server - a path. JPEG and PNG both work, and PNG transparency composites over whatever sits behind it, not a white box.

const logo = new Uint8Array(await (await fetch("/logo.png")).arrayBuffer());

<Image src={logo} width={120} height={40} fit="contain" />;

A URL is fetched while the document is prepared, before the layout runs, so its size is known when the page is laid out.

Render options

renderToPdf (and renderToPdfString) take the engine's render options as their second argument:

const bytes = await renderToPdf(<Invoice {...data} />, {
  onOverflow: "warn",
  accessible: true,
  encrypt: { userPassword: "secret" },
  onDiagnostic: (d) => console.log(d.code, d.message),
});
  • onOverflow - what happens when a single unbreakable element is taller than the page: "error" (default) throws, "warn" places it clipped and logs, "ignore" places it clipped.
  • accessible - a tagged PDF with a structure tree for screen readers, see Accessibility.
  • encrypt - AES-256 password protection, see Encryption.
  • onDiagnostic - everything the engine has to say about the document, one call per finding, see Output.

A descriptor for a server

toDocumentDescriptor(element) stops before the PDF and returns the engine's descriptor: a plain { type, props, children } tree. To post it as JSON, encode font and image bytes yourself - JSON has no Uint8Array - and decode them on the other side. Functions do not survive JSON at all, so a document with a Canvas, a Flow or a PageBuilder goes through renderToPdf.