---
title: "The PDF target — quario"
description: "Rendering a quario definition to a paginated PDF without a headless browser: page geometry, fonts, page breaks and deterministic bytes."
url: "https://getquario.com/docs/diving-deeper/pdf-target"
---

# The PDF target

## Intent

`@quario/pdf` turns one compiled report into a paginated document. It typesets
the pages itself, so nothing in the path needs a headless browser, a print
stylesheet, or a screenshot step. What you deploy is a library, not a second
runtime inside your container.
[How that compares with other JavaScript PDF generators →](https://getquario.com/guides/javascript-pdf-generator.md)

## Design decisions

**This target is a painter.** It lays the document out itself, which is what lets
it promise where every line falls. The trade is against
[the DOCX target](https://getquario.com/docs/diving-deeper/docx-target.md), which states the look and
lets Word turn the pages. [Painters, flow targets, and a fragment](https://getquario.com/docs.md#painters-flow-targets-and-a-fragment)
sets out the family.

**The target typesets, and `@cantoo/pdf-lib` assembles.** Pagination,
keep-together, table geometry and the style mapping all belong to this package.
Turning the finished layout into file bytes belongs to that library, a maintained
pdf-lib fork. The split is why the target stays runtime-neutral and runs in a
browser as readily as in Node.

**Paper comes from the host or the document, one key at a time.** A
definition that works on any paper leaves `page.size` to the factory call. One
file then prints on A4 for one customer and Letter for the next. A document that
only works one way declares the paper itself. That might be a landscape
statement, or a pinned header that knows what room it needs. Declaring the same
key in both places is a render error rather than a silent winner.
`orientation` in a document beside a host `size` isn’t both, because it turns
whichever paper is in force.

**Text renders at 10 points when nothing declares a size.** A document’s type
size is the document’s own, so it’s the report’s `style.size` and not a host
option. The XLSX target writes the same 10 for the same reason.

**Styled runs reach the page.** Each run draws in its own face, size, and
colour. The target draws underline and strikethrough per run, over that run’s
width and in its colour. A run’s `background` paints a highlight behind its
text. A line takes the largest size among its runs.

**The target pins dates, so the bytes repeat.** The document carries no
creation timestamp, and `meta` is opt-in and never holds a date. The same
definition, data and options produce a byte-identical file. That’s what makes
a rendered PDF safe to cache, hash, or diff in a test.

**Keep-together needs no schema fields.** Group headers travel with their first
content unit, and the last data row keeps its total block. A group’s headers
repeat at the top of every page it continues onto. You declare none of that,
because a definition describing where its own page breaks fall would stop being
portable across page sizes.

**A table under a group keeps one set of column widths.** The layout sizes each
column from the widest content it holds in any instance, so the columns line up
down the whole document. A table that renders once keeps the widths its own
content asks for. The viewer and the editor show the same widths, because they
paint the same layout.

**Six typesetting behaviours are best-effort.** Widow and orphan handling, the
page-column gutter and balancing, and how a tall row slices are this
implementation’s current answers. So are the oversize image, image byte reuse,
and where an image’s page break lands. Everything else in the target’s
contract is normative. Don’t write a definition that depends on the six.

**Embedding TrueType needs the optional `fontkit` peer.** The base-14 families
need nothing extra, so a report in Latin text carries no additional dependency.
The target names a missing package rather than failing obscurely.

**Mapping a generic name replaces its base-14 faces.** Unlike the HTML target’s
mapping, this one reaches text that declares no `family` at all. A generic is
the face that baseline already resolves to. A host can say what “the mono one”
is for a document that names no typeface of its own.

**An unknown character renders as a question mark.** Cell text is untrusted
data, and one stray character must not fail a report. WinAnsi is the base-14
limit, and a TrueType face’s own cmap is its limit.

**The unlicensed marking draws once per page.** The target draws it last, over
the content and the page furniture, so no filled cell hides it. Its presence is
normative and its geometry isn’t. The marking depends only on the license
state and the page size, so determinism holds either way.

## API walkthrough

### Setting the page up

**`pdf({ page })`** takes the geometry at the factory call and validates it
there, so a bad option throws before the engine walks any data. It takes `page`,
`meta` and `fonts`, and nothing else. A key outside that set, or one of the wrong
type, throws a `TypeError` naming the path rather than passing unread.

**`page.size`** is `'A4'`, `'letter'`, or a `[width, height]` pair in points.
A4 is the default at 595.28 by 841.89 points. An unknown size name fails at
compile time. A document may name `A4` or `letter` in its own `page.size`, and
a measured pair stays a host option.

**`page.orientation`** is the document’s alone, `portrait` or `landscape`. It
turns the paper without changing which paper it is.

**`page.margin`** is a number of points applied to all four sides, or `{ top,
right, bottom, left }` naming each, defaulting to 54. That default isn’t a pass for a header that declared a height. Such a
header pins a box from the page top and starts the next band at the pin.

```ts title="render.ts"
import { pdf } from '@quario/pdf'

const bytes = await report.render(pdf({ page: { size: 'A4', margin: 54 } }), data)
```

### Naming the document

**`meta.title`**, **`meta.author`** and **`meta.subject`** write an optional
`/Info` dictionary. They’re strings only, never dates, which is what keeps the
output reproducible.

**A grouped report carries an outline.** Bookmarks mirror the group tree, one
open entry per instance that drew something. Each entry nests by depth and
takes the instance’s `label` as its title. Where the group declares none it
uses the instance’s first group-header text. The label exists to say
what the visible text doesn’t. A group instance that drew nothing has no position
to target and gets no entry. Ungrouped reports carry no outline.

**A linked run becomes a link annotation.** Each rectangle the layout drew for
a run carrying an `href` gets a `/Link` with no border. That’s a URI action for
an external URL, and a destination inside the document where the `href` names a
group’s `label`. A run wrapping across two lines is two rectangles, so it’s two
annotations. A link reaches only an instance that declares that `label`, never
one whose outline title happens to read the same. The target drops a link to a
label no instance declares rather than inventing one.

**A collapsed group draws its header and its footer.** A page has nowhere to
put a row a reader could open. An instance whose group declares `collapsed`
therefore renders as those two bands, leaving out the content between them.
Nothing changes its aggregates, so the footer still totals them.

### Embedding fonts for full Unicode

**`fonts`** maps a family name to TrueType bytes. Styles then select it by
name, as `"family": "Inter"`, and names match case-insensitively.

**`regular`**, **`bold`**, **`italic`** and **`boldItalic`** are one family’s
variants. A missing variant uses the family’s regular.

**`fontkit`** is the optional peer that embedding requires. Embedded text uses
the font’s own metrics, subsets to the glyphs the document uses, and carries a
ToUnicode map. Copying text out of the finished PDF returns the original
characters.

**Mapping `sans`, `serif` or `mono`** replaces that generic’s base-14 faces and
reaches text that declares no family at all.

Hold the font record and its buffers across renders rather than rebuilding
them. They’re the identity the target remembers a face by, so a fresh
buffer means parsing the file again.

Which scripts come out right is a fact about the face you supply and the parser
reading it. Shaping isn’t quario’s, and the target claims nothing per-script of
its own. Fonts are host assets you trust, the same as registered functions.

### Controlling where pages break

**`break`** places the page turns around a group’s instances — `"before"`,
`"between"`, `"after"` or `"around"`, as [Laying a document out in
bands](https://getquario.com/docs/diving-deeper/bands.md) sets out. An instance already at a page top
doesn’t force an empty one.

**`reset: "page"`** restarts the numbering at that instance without turning a
page itself, so a three-page invoice inside a batch reads `1 of 3` rather than
`47 of 200`. It requires `break` to be `"before"` or `"around"`.

**A short instance moves whole rather than splitting.** An instance that would
cross a page break travels to the next page instead, wherever a page could hold
all of it. A group therefore doesn’t leave a line or two stranded at the foot.
An instance taller than a page still breaks. This is best-effort, like the height
probe: what’s guaranteed is that a group header travels with its first content.

**`page.header`** and **`page.footer`** are furniture drawn on every page, the
header below the top margin and the footer above the bottom. The target
subtracts their heights from the body area and measures them once per render, so
keep them short.

**`visible: "=page.number > 1"`** suppresses a band on the first page of the
current sequence, which is the cover-page recipe.

The target probes band heights once, treating them as both a first and a
later page of a two-page document. Anything that varies with the real page
count is invisible to that probe. Gate visibility on `page.number`, and
keep `page.total` in text short enough that it can’t change the band’s
height.

### Taking the bytes

**`render(pdf(options), data)`** resolves the complete document as a
`Uint8Array`. Rendering is async and yields to the event loop between batches,
so a long report doesn’t block your server.

**Writing the file belongs to the host.** Put the bytes on disk, in object
storage, or straight into a response body.

Compilation errors throw synchronously, and render errors reject with their
location attached. A failure names the band and the source behind it rather
than a byte offset in a PDF.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://getquario.com/"},{"@type":"ListItem","position":2,"name":"Docs","item":"https://getquario.com/docs"},{"@type":"ListItem","position":3,"name":"The PDF target","item":"https://getquario.com/docs/diving-deeper/pdf-target"}]}
```
