# Technical Documentation: embedpdf/embed-pdf-viewer

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/embedpdf/embed-pdf-viewer)

### Overview (README.md)

<div align="center">
  <a href="https://www.embedpdf.com">
    <img alt="EmbedPDF logo" src="https://www.embedpdf.com/logo-192.png" height="96">
  </a>

  <h1>EmbedPDF</h1>

  

<a href="https://github.com/embedpdf/embed-pdf-viewer/blob/main/LICENSE"><img alt="License: Apache 2.0" src="https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=for-the-badge&labelColor=000000"></a>
<a href="https://snippet.embedpdf.com/"><img alt="Live demo" src="https://img.shields.io/badge/Try%20the%20Live%20Demo-ff1493.svg?style=for-the-badge&labelColor=000000"></a>
<a href="https://discord.gg/mHHABmmuVU"><img alt="Join our Discord" src="https://img.shields.io/discord/1351694551047475313?logo=discord&style=for-the-badge&labelColor=000000"></a>

</div>

> [!WARNING]
> **EmbedPDF v3 is under active development and is not yet recommended for production use.** For the current stable release, use the [`v2` branch](https://github.com/embedpdf/embed-pdf-viewer/tree/v2).

# Open‑Source JavaScript PDF Viewer

**EmbedPDF** is a framework‑agnostic, Apache‑2.0‑licensed PDF viewer that drops into _any_ JavaScript project. Whether you build with **React, Vue, Svelte, Preact,** or vanilla JS, EmbedPDF delivers a smooth, modern reading experience and a clean developer API.

---

## 📚 Documentation

Full docs, installation guides, API reference, and examples:

👉 **[https://www.embedpdf.com](https://www.embedpdf.com)**

## 🚀 Live Demo

Try it now — load your own PDF or use the sample:

👉 **[https://app.embedpdf.com](https://app.embedpdf.com)**

---

## 💖 Sponsors

We are grateful for the support of our sponsors!

### Silver Sponsors

<div align="left">
  <a href="https://www.withloveinternet.com?utm_source=embedpdf&utm_campaign=oss" target="_blank"><img src=".github/assets/sponsor-wli.png" alt="With Love Internet" height="80"></a>
  &nbsp;&nbsp;<a href="https://align.lawyer?utm_source=embedpdf&utm_campaign=oss" target="_blank"><img src=".github/assets/sponsor-align.jpeg" alt="Align" height="80"></a>
</div>

### Bronze Sponsors

<div align="left">
  <a href="https://www.accrual.com?utm_source=embedpdf&utm_campaign=oss" target="_blank"><img src=".github/assets/sponsor-accrual.jpg" alt="Accrual" height="64"></a>
  &nbsp;&nbsp;<a href="https://layer.team/?utm_source=embedpdf&utm_campaign=oss" target="_blank"><img src=".github/assets/sponsor-layer.png" alt="Layer" height="64"></a>
  &nbsp;&nbsp;<a href="https://www.lefebvre-group.com?utm_source=embedpdf&utm_campaign=oss" target="_blank"><img src=".github/assets/sponsor-lefebvre.png" alt="Lefebvre" height="64"></a>
  &nbsp;&nbsp;<a href="https://forml.eu?utm_source=embedpdf&utm_campaign=oss" target="_blank"><img src=".github/assets/sponsor-forml.png" alt="forml" height="64"></a>
  &nbsp;&nbsp;<a href="https://hive.com/?utm_source=embedpdf&utm_campaign=oss" target="_blank"><img src=".github/assets/sponsor-hive.png" alt="Hive" height="64"></a>
</div>

---

## ✨ Features

- Annotations (highlight, sticky notes, free text, ink)
- True redaction (content is actually removed)
- Search, text selection, zoom, rotation
- Smooth, virtualized scrolling
- Pluggable architecture & tree-shakable plugins

## 🤝 Contributing

We love contributions! To get started, read our [contributing guide](CONTRIBUTING.md) and jump into the [GitHub discussions](https://github.com/embedpdf/embed-pdf-viewer/discussions).

## 📄 License

Everything in this repository is licensed under the [Apache License, Version 2.0](LICENSE), with one exception: [`cloudpdf/server`](cloudpdf/server) — the self-hostable CloudPDF server — is **Fair Source**, under the [Fair Core License, FCL-1.0-ALv2](cloudpdf/server/LICENSE). Its source may be inspected, copied, and modified for purposes permitted by the FCL. **That does not make the server free to run:** while a release remains under the FCL, running or self-hosting it requires a valid CloudPDF license (a license key for a connected deployment or a signed certificate for an air-gapped deployment). You may not move, change, disable, or circumvent the license-key functionality; enable protected functionality without a valid license; or remove protected functionality. Competing Uses, as defined in the FCL, are also prohibited. Each release automatically becomes Apache-2.0 two years after publication. See [LICENSING.md](LICENSING.md) for the full licensing map, including website content.

## Third-Party Licenses

The engine is powered by [EmbedPDF Runtime](https://github.com/embedpdf/runtime), our fork of [PDFium](https://pdfium.googlesource.com/pdfium/), which is licensed under the [Apache License, Version 2.0](https://pdfium.googlesource.com/pdfium/+/refs/heads/main/LICENSE). PDFium is a Google project; EmbedPDF is not affiliated with or endorsed by Google.

---

### Conventions README (`docs/conventions/README.md`)

# Conventions

How the EmbedPDF client stack is built, and the rules every change follows.
These documents describe the code as it is. When the code and a document
disagree, fix one of them in the same pull request.

Start with this page, then read [`architecture.md`](./architecture.md).

## The model

A plugin provides one capability per scope (the workspace, or each open
document). The capability is its only public surface: synchronous reads,
verbs that do work, and events. A plugin talks to the engine only through
`ctx.doc`, and to other plugins only through their capabilities.

### Four kinds of state

| Kind           | What it is                                                   | Home                                                                    | How it changes                                                                   |
| -------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Mirror         | A local copy of data the engine owns                         | `ctx.mirror(spec)`, `ctx.pageMirror(spec)`                              | Only by folding confirmed engine events (every origin) and by loads              |
| Pending change | A change this session staged that the engine hasn't answered | the document's change queue (`ctx.changes`), shown by mirrors' `view()` | Staged by a verb or gesture; leaves the views once answered and held, or refused |
| Session        | State the client owns that is not in the PDF                 | `ctx.state`                                                             | Only through named pure transitions in `model.ts`                                |
| Resource       | Handles, rasters, workers, registries of functions, caches   | closures, owned through `ctx.cleanup` and `ctx.acquire`                 | Any way; readers are woken with `ctx.notify()`                                   |

### Three kinds of events

| Kind         | Meaning                              | Fired from                                                         |
| ------------ | ------------------------------------ | ------------------------------------------------------------------ |
| Fact         | Something changed in the document    | Where the confirmed document event is applied, with `event.origin` |
| State change | A session value changed              | The plugin's one `ctx.state.onChange` listener                     |
| Occurrence   | An operation ran, finished or failed | Where the operation reaches that point                             |

## Core rules

1. A capability is a plugin's only public surface. Other plugins and adapters
   read through it, never through a plugin's state.
2. Every piece of state has one kind and one home: mirror, pending change,
   session or resource. State is plain data; everything else is a resource.
3. A mirror changes only by its fold and by loads. Its fold is pure and the
   same for every origin. Verbs never write it.
4. Session state changes only through named pure transitions applied with
   `ctx.state.update`. A transition that returns the same object changes
   nothing.
5. A verb calls the engine and returns. The engine publishes the confirmed
   event before the call resolves, so the verb's `await` already sees its own
   write: no refetch, no waiting, no hand-made events.
6. Every event fires from exactly one place. Fact events carry
   `event.origin` as it is; no plugin builds an origin by hand. Loads announce
   `onResynced`.
7. Reads are synchronous, pure and identity-stable (`memo`, `memoByKey`).
   They never start engine work.
8. A plugin declares every token it resolves in `requires` or `optional`, and
   imports sibling tokens from their `/contract` entries.
9. Registrations with siblings and subscriptions happen in `connect`, and
   everything acquired is owned: `ctx.cleanup`, `ctx.listen`, `ctx.acquire`.
10. A capability rejects with `PluginError` only, and every verb that can
    refuse has a `can*` twin.
11. Names and comments follow [`naming.md`](./naming.md) and
    [`comments.md`](./comments.md).

## Map

| File                                                     | Covers                                                                                                                                   |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| [`architecture.md`](./architecture.md)                   | Layers, scopes and bring-up, the four kinds of state, data flow, reads, reactions, pure cores, plugin anatomy                            |
| [`state-and-sync.md`](./state-and-sync.md)               | `ctx.state`, `notify`, `watch`, `waitFor`, mirrors, page mirrors, pending changes, resources, reactions, the sync rules                  |
| [`events.md`](./events.md)                               | Fact, state-change and occurrence events, event sources, naming, `EventOrigin`, `onResynced`, subscribing                                |
| [`plugins.md`](./plugins.md)                             | Plugin files, manifest, tokens and contracts, package entries, controller, `connect`, dependencies, lifetime, errors, a complete example |
| [`testing.md`](./testing.md)                             | Where tests live, the test context, fake engines, mirror tests, kernel tests, conformance suites, type tests                             |
| [`naming.md`](./naming.md)                               | Identifiers, the glossary, type suffixes, the capability vocabulary                                                                      |
| [`comments.md`](./comments.md)                           | What a comment may say, and how it is checked                                                                                            |
| [`packages.md`](./packages.md)                           | Where a package lives and what it is called                                                                                              |
| [`permissions.md`](./permissions.md)                     | Authorization-aware plugins: `can*` twins, gates, per-record authority                                                                   |
| [`viewer-doors.md`](./viewer-doors.md)                   | One viewer, several deliveries: the entry points that choose the engine                                                                  |
| [`engine-services.md`](./engine-services.md)             | Layout of `@embedpdf/engine-services`: readers, mutators, the worker host                                                                |
| [`runtime-confinement.md`](./runtime-confinement.md)     | Thread-confined PDFium: per-thread state, the worker pool, the threading gate                                                            |
| [`engine-host-isolation.md`](./engine-host-isolation.md) | Running engines in supervised child processes, and the threat model                                                                      |
| [`server-adapters.md`](./server-adapters.md)             | The CloudPDF server's adapter families: secrets, KMS, storage, CDN, import                                                               |
| [`server-migrations.md`](./server-migrations.md)         | Forward and rollback database migrations for the CloudPDF server                                                                         |
| [`docs-architecture.md`](./docs-architecture.md)         | How the documentation site renders one source per framework                                                                              |

Package build and export rules are in
[`tooling/build/README.md`](../../tooling/build/README.md). Changesets are
described in [`.agents/skills/embedpdf-changesets/SKILL.md`](../../.agents/skills/embedpdf-changesets/SKILL.md).

---

### Content Engine Index (`docs/content/engine/index.mdx`)

---
title: Engine
description: Open, read, change and save PDF files from JavaScript, with one typed API.
---

import { Cards, Card } from '@/components/docs/cards';

# The PDF engine

<Engine local>

The EmbedPDF engine opens PDF files in the browser and lets you render their pages, read and
search their text, add annotations, fill forms, edit pages and save the result. It runs PDFium,
the PDF library inside Chrome, in a Web Worker, so your page stays responsive. Nothing is
uploaded: the file never leaves the device.

The ready-made viewer and the headless components are built on this engine. Use it directly
when you want PDF features without a viewer: a thumbnail job, a form filler, an export script.

</Engine>

<Engine cloud>

The CloudPDF engine gives your app a full PDF engine over HTTPS. Your files stay on your
CloudPDF server. The engine renders their pages, reads and searches their text, stores
annotations and form values, and checks every call against the permissions in the user's
token.

The CloudPDF viewer and the headless components are built on this engine. Use it directly when
you want PDF features without a viewer: a thumbnail job, a form filler, an export script.

</Engine>

<Engine local>

```ts
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
const doc = await engine.open({ kind: 'bytes', bytes });

const { pages } = await doc.pages.list();
const image = await doc.page(pages[0].ref).render.image();
```

</Engine>

<Engine cloud>

```ts
import { cloudEngine } from '@cloudpdf/engine';

const engine = cloudEngine();
const doc = await engine.open({ kind: 'token', token: () => fetchDocumentToken(documentId) });

const { pages } = await doc.pages.list();
const image = await doc.page(pages[0].ref).render.image();
```

</Engine>

## What you can do

- **[Render pages](/docs/engine/documents/rendering)** as images, at any size.
- **[Read](/docs/engine/text/extraction), [search](/docs/engine/text/search) and
  [select](/docs/engine/text/selection-and-geometry)** the text on a page, and find out where
  each character is.
- **[Add and edit annotations](/docs/engine/annotations)**: highlights, notes, shapes, stamps,
  measurements. Copy them to another document with
  [import & export](/docs/engine/annotations/import-export).
- **[Fill forms](/docs/engine/forms)**, create form fields and
  [sign](/docs/engine/forms/signatures) documents.
- **[Change pages](/docs/engine/documents/pages)**: rotate, reorder, delete, insert, merge and
  split.
- **[Redact](/docs/engine/documents/redaction)** content for good, and
  **[save](/docs/engine/documents/saving)** the result.
- **[Group edits into changes](/docs/engine/documents/changes)** that apply all or none, and undo
  or redo any of them.

Every call is async, can be cancelled, and fails with a typed error you can branch on. The same
code runs on the local engine and on the CloudPDF engine: only how you create the engine and
open a file differs.

A few things exist only on the local engine: its fonts, `warmup()`, raw pixels, and saving a
document's layer or to a file. A document opened with `localEngine()` has them. Code that takes
any engine checks first:

```ts
import { isLocalDocument } from '@embedpdf/engine';

if (isLocalDocument(doc)) {
  const layer = await doc.downloadLayer();
}
```

`isLocalEngine(engine)` does the same for an engine.

<Engine local>

## Local or cloud

This engine runs in the browser: no server, no upload, and it works offline. That makes it the
right choice when the user already has the file, or when the file may not leave their device.

<CloudPdfCallout
  title="Keep the file on your server"
  href="https://www.cloudpdf.com/docs/engine"
  cta="Read about the CloudPDF engine"
>

When the file lives on your server, the CloudPDF engine runs the same API there and checks every
call. A user sees the first page as an image, without downloading the whole PDF or the engine
(about 3 MB compressed), and a user who may not download the file never receives it. Your code
stays the same: swap `localEngine()` for `cloudEngine()`.

</CloudPdfCallout>

</Engine>

## Next steps

<Cards>
  <Card
    title="Quick start"
    description="Open a PDF, render a page, search it and add a highlight."
    href="/docs/engine/quick-start"
  />
  <Card
    title="Setup"
    description="Install the engine and create it once for your app."
    href="/docs/engine/setup"
  />
  <Card
    title="Annotations"
    description="Read, add and change annotations of every kind."
    href="/docs/engine/annotations"
  />
</Cards>

---

### Content Headless Index (`docs/content/headless/index.mdx`)

---
title: Headless
description: Build your own PDF viewer from plugins, components and hooks, in React, Vue, Svelte or Angular.
---

import { Cards, Card } from '@/components/docs/cards';

{/* Served at `…/headless/<framework>` with NO trailing slash: a relative href
    resolves against `…/headless/`, so pages are `quick-start`,
    `viewing/stage`, `concepts/how-it-works`. */}

# Build your own PDF viewer

The headless packages give you everything a PDF viewer does, without a look of their own: pages
that scroll and zoom, crisp rendering, text selection and search, annotations, forms and
signatures. You build the interface in <Framework />, and nothing appears on screen unless you put
it there.

<Engine local>

<Snippet name="start/overview" />

</Engine>

<Engine cloud>

<Snippet name="start/overview-cloud" />

</Engine>

That's a complete viewer: the pages scroll, zoom with a pinch or Ctrl and the wheel, and stay sharp
at every zoom. Everything else is a plugin you add, and a few lines of your own UI.

## Headless or the ready-made Viewer?

EmbedPDF comes in two forms, built on the same engine and plugins:

- **The [Viewer](/docs/viewer)** is finished: a toolbar, sidebars, annotation tools and search,
  ready in minutes. You configure it and theme it.
- **Headless** is the parts: you decide what the toolbar looks like, where the search box goes, and
  what happens when someone clicks an annotation.

Choose headless when the viewer is part of your product's own interface. You can start with the
Viewer and move to headless later: the plugins and their options are the same.

## How it's put together

A headless viewer has five parts. You meet them on every page:

- **The engine** opens and reads PDF files. It runs in the browser, or on your server with
  CloudPDF.
- **`<Viewer>`** starts the engine, opens documents, and holds the plugins.
- **Plugins** add one feature each: search, annotations, forms. Register the ones you need; the
  rest never reach your bundle.
- **Layers** draw inside each page: the page itself, the text selection, the annotations.
- <Fw only="react">**Hooks**</Fw><Fw only="vue">**Composables**</Fw><Fw only="svelte">**Functions**</Fw><Fw only="angular">**Services**</Fw> connect your own UI: <Fw only={['react', 'vue', 'svelte']}>`useSearch()`
  to search, `useSearchState()` to show the results, `useSearchEvent()` to react when something
  happens.</Fw><Fw only="angular">`inject(EpdfSearch)` gives you the plugin's methods to search,
  its values as signals to show the results (`search.hitCount()`), and its events as streams to
  react when something happens (`search.completed$`).</Fw>

[How it works](concepts/how-it-works) explains each in a few minutes.

## Every plugin

Each plugin is one import line. Registering a plugin and importing its components come from the
same place:

| Plugin | What it gives you | Import from |
| :-- | :-- | :-- |
| [Stage](viewing/stage) | Scrolling, zoom, layouts, page navigation | `@embedpdf/react/stage` |
| [Render](viewing/render) | Sharp pages at every zoom, deep-zoom tiles | `@embedpdf/react/render` |
| [Links](viewing/links) | Clickable links to pages and websites | `@embedpdf/react/link` |
| [Selection](text/selection) | Selecting and copying text | `@embedpdf/react/selection` |
| [Search](text/search) | Finding text, and showing every match | `@embedpdf/react/search` |
| [Annotations](annotations) | Highlights, notes, shapes, free text, and your own tools | `@embedpdf/react/annotation` |
| [Stamps](annotations/stamps) | Libraries of stamps, placed by click or by code | `@embedpdf/react/stamp` |
| [Measurements](annotations/measurements) | Distances, perimeters and areas at a real scale | `@embedpdf/react/measurement` |
| [Redaction](annotations/redaction) | Removing content for good | `@embedpdf/react/redaction` |
| [Forms](forms) | Filling and building PDF forms | `@embedpdf/react/form` |
| [Signatures](forms/signatures) | Signing documents and checking signatures | `@embedpdf/react/signature` |
| [Page edit](documents/pages) | Rotating, reordering, inserting and deleting pages | `@embedpdf/react/page-edit` |
| [Metadata](documents/metadata) | A document's title, author and your own fields | `@embedpdf/react/metadata` |
| [Actions](documents/actions) | What a PDF does by itself: buttons, scripts, printing | `@embedpdf/react/actions` |
| [View manager](documents/tabs) | Several documents side by side | `@embedpdf/react/view-manager` |
| [Interaction](ui/tools) | The active tool, cursors and touch | `@embedpdf/react/interaction` |
| [Commands](ui/commands) | Named actions with labels, icons and shortcuts | `@embedpdf/react/commands` |
| [Shell](ui/panels) | Which panels, dialogs and menus are open | `@embedpdf/react/shell` |
| [Translations](ui/translations) | Your UI in the user's language | `@embedpdf/react/i18n` |

## Local or cloud

The same viewer runs on either engine. Only how you create the engine and open a document differs;
every plugin, <Word of="hook" /> and component stays the same.

<Engine local>

The local engine runs in the browser: no server, no upload, and it works offline. It's the right
choice when the user already has the file, or when the file may not leave their device.

<CloudPdfCallout
  title="Show a 500-page PDF before it downloads"
  href="https://www.cloudpdf.com/docs/headless"
  cta="Build your viewer on CloudPDF"
>

With the CloudPDF engine, the file stays on your server and pages arrive as images, so the first
page shows at once, without downloading the whole PDF or the engine. Every call is checked on the
server, several people can review one document together, and every change is stored as it's made.
Your viewer stays the same: swap `localEngine()` for `cloudEngine()`.

</CloudPdfCallout>

</Engine>

<Engine cloud>

The CloudPDF engine keeps the file on your server. Pages arrive as images, so the first page
shows at once; every call is checked against the user's token; and annotations and form values are
stored as people make them, with everyone who has the document open seeing each other's changes.

</Engine>

## Next steps

<Cards>
  <Card
    title="Quick start"
    description="A viewer with text selection, a zoom control and a highlighter, step by step."
    href="quick-start"
  />
  <Card
    title="Setup"
    description="Install for your framework, create the engine, and render on the server."
    href="setup"
  />
  <Card
    title="How it works"
    description="Plugins, how your UI talks to them, layers, and which document a component talks to."
    href="concepts/how-it-works"
  />
</Cards>

---

### Content Viewer Index (`docs/content/viewer/index.mdx`)

---
title: Viewer
description: A complete PDF viewer you add with one tag, then make your own with a few lines of config, in plain HTML, React, Vue, Svelte or Angular.
---

import { Cards, Card } from '@/components/docs/cards';

{/* Served at `…/viewer/<framework>` with NO trailing slash: a relative href
    resolves against `…/viewer/`, so pages are `quick-start`,
    `customize/layout`, `concepts/how-it-works`. */}

# A complete PDF viewer, ready to embed

The viewer is everything people expect from a PDF app, in one tag: a toolbar, thumbnails, search,
annotations, comments, forms, signatures and stamps. It fits a phone and a wide screen, speaks the
user's language, and works with a keyboard and a screen reader.

<Snippet name="viewer/start/first" />

That's the whole integration. The rest of these docs are about making it yours.

## Make it yours

Everything you change is a few lines of config, the same in every framework. Start at the top and
stop when it's enough:

| You want to | Change | Page |
| :-- | :-- | :-- |
| Match your brand | Your accent color, fonts, light or dark | [Theming](customize/theming) |
| Leave features out, or make it read-only | `disable: ['forms', 'redaction']`, `readOnly: true` | [Turn features off](features/turning-off) |
| Move or remove buttons | One line per change | [Layout](customize/layout) |
| Put a toolbar at the bottom, on the side, or several | Where each bar goes | [Layout](customize/layout) |
| Add your own button | A command: a label, an icon, what it does | [Buttons & commands](customize/commands) |
| Add your own menu or panel | A list of items, or your component | [Menus](customize/menus), [Panels](customize/panels) |
| Put your own component in the toolbar | Your React, Vue, Svelte or Angular component | [Your own components](customize/components) |
| Control the viewer from your app | The `viewer` object | [The viewer in code](code/viewer) |

You never write layout code for phones, menus that open the right way, or keyboard focus. You say
what goes where, and the viewer takes care of how it behaves. A menu in a bottom toolbar opens
upward; the thumbnails slide up from the bottom on a phone; a button that doesn't fit moves into
the "More" menu.

## The viewer or headless?

EmbedPDF comes in two forms, built on the same engine and the same plugins:

- **The viewer** is finished. You configure it, theme it, and add your own buttons, menus, panels
  and components.
- **[Headless](/docs/headless)** is the parts. You build the whole interface yourself, and
  nothing appears on screen unless you put it there.

Start with the viewer. Every feature is a [headless plugin](code/plugins) with the same settings,
and inside the viewer your own components can use the headless hooks. If you outgrow the viewer,
what you learned still applies.

<Engine local>

## Local or cloud

The viewer runs on the local engine by default: the PDF opens in the browser, with no server and no
upload, and it works offline.

<CloudPdfCallout
  title="Open a 500-page PDF at once"
  href="https://www.cloudpdf.com/docs/viewer"
  cta="Use the viewer with CloudPDF"
>

With CloudPDF, the file stays on your server and pages arrive as images, so the first page shows
at once, without downloading the whole PDF or the engine. Every action is checked on the server,
and annotations and comments are shared live between everyone who has the document open. The
viewer and your config stay the same.

</CloudPdfCallout>

</Engine>

<Engine cloud>

## The CloudPDF viewer

The file stays on your server. Pages arrive as images, so the first page shows at once; every
action is checked against the user's token; and annotations, comments and form values are stored
as people make them, with everyone who has the document open seeing each other's changes.

</Engine>

## Next steps

<Cards>
  <Card
    title="Quick start"
    description="Install, show a PDF, add your color and your own button."
    href="quick-start"
  />
  <Card
    title="Layout"
    description="Toolbars at any edge, several of them, and every button where you want it."
    href="customize/layout"
  />
  <Card
    title="Your own components"
    description="Your React, Vue, Svelte or Angular components inside the viewer."
    href="customize/components"
  />
</Cards>

---

### Content Engine Annotations Index (`docs/content/engine/annotations/index.mdx`)

---
title: Annotations
description: List, add, change, reorder and delete annotations, with the fields and styles every kind shares.
---

import { Callout } from '@/components/docs/callout';
import { CardGrid, GridCard } from '@/components/docs/cards';

# Annotations

Annotations are what people add on top of a page: highlights, sticky notes, shapes, stamps,
free text, links. They're stored in the PDF, so every PDF app shows them, and they stay separate
from the page's own content, so you can change or remove them later.

```ts
const page = doc.page(pageRef);

const { annotation } = await page.annotations.create({
  subtype: 'square',
  box: { x: 72, y: 92, width: 200, height: 100 },
  color: '#0078ff',
});

await page.annotations.update(annotation.ref, { contents: 'Check this table' });
await page.annotations.delete(annotation.ref);
```

## Kinds

Each kind has its own page with an example and its fields:

<CardGrid columns={2}>
  <GridCard
    icon="annotation"
    title="Highlights & text markup"
    description="Highlight, underline, squiggly and strikeout over text."
    href="/docs/engine/annotations/text-markup"
  />
  <GridCard
    icon="note"
    title="Notes & replies"
    description="Sticky notes, their popups, and threads of replies."
    href="/docs/engine/annotations/notes-and-replies"
  />
  <GridCard
    icon="free-text"
    title="Free text"
    description="Text boxes and callouts, with fonts and rich text."
    href="/docs/engine/annotations/free-text"
  />
  <GridCard
    icon="shape"
    title="Shapes & drawing"
    description="Rectangles, ellipses, lines, polygons and freehand ink."
    href="/docs/engine/annotations/shapes"
  />
  <GridCard
    icon="stamp"
    title="Stamps"
    description="Images and drawings placed on a page."
    href="/docs/engine/annotations/stamps"
  />
  <GridCard
    icon="link"
    title="Links"
    description="Areas that go to another page or open a website."
    href="/docs/engine/annotations/links"
  />
  <GridCard
    icon="attachment"
    title="File attachments"
    description="A file pinned to a spot on the page."
    href="/docs/engine/annotations/file-attachments"
  />
  <GridCard
    icon="measurement"
    title="Measurements"
    description="Distances, perimeters and areas at a real-world scale."
    href="/docs/engine/annotations/measurements"
  />
</CardGrid>

## List annotations

Every annotation in the document, or the ones on one page:

```ts
const { annotations } = await doc.annotations.list(); // every page
const { annotations } = await doc.annotations.list({ pages: [pageA, pageB] }); // some pages
const { annotations } = await doc.page(pageRef).annotations.list(); // one page
```

A list is cheap even for a whole document: the engine reads the annotations without loading the
pages. Each annotation names its `page`, and the list's `pages` has one entry per page it read.

Each annotation has a `subtype` (its kind), a `rect` (where it is on its page) and a `ref` (its
address). Keep the `ref`: it's how you change or delete the annotation later.

Positions are in points from the top-left corner of the page, with y going down, like every
position in the engine: a box is `{ x, y, width, height }` and a point `{ x, y }`. See
[Coordinates](/docs/engine/concepts/coordinates).

```ts
for (const annotation of annotations) {
  console.log(annotation.subtype, annotation.rect, annotation.contents);
}
```

A list includes kinds the engine can't create yet: they come back as `subtype: 'unsupported'`,
with their position and common fields. They stay in the PDF untouched.

Form widgets are annotations too, of kind `'widget'`, but they're the form's: a list here never
holds one, and `doc.forms.list()` returns them with the fields. You place and change them through
the form; see [Creating form fields](/docs/engine/forms/authoring).

## Fields every annotation has

Besides `subtype`, `rect` and `ref`, every kind has these fields:

| Field      | What it is                                                                                               |
| :--------- | :------------------------------------------------------------------------------------------------------- |
| `contents` | A comment, the text PDF apps show for the annotation.                                                    |
| `subject`  | A short title PDF apps show with the comment, such as `'Question'`.                                      |
| `nm`       | A unique name on its page. See [Refs & identity](/docs/engine/concepts/identity).                        |
| `reply`    | The annotation it answers, or `null`. See [Notes & replies](/docs/engine/annotations/notes-and-replies). |
| `popup`    | The ref of its popup window, or `null`. See [Popups](/docs/engine/annotations/notes-and-replies#popups). |
| `groupId`  | The group it belongs to, when you work with groups.                                                      |

Each annotation also has the [flags](#flags) and the fields that say [who wrote it](#who-wrote-it).

## Add an annotation

`create()` takes the kind and its fields, and returns the new annotation:

```ts
const { annotation } = await page.annotations.create({
  subtype: 'circle',
  box: { x: 60, y: 392, width: 120, height: 100 },
  interiorColor: '#1e90ff',
});
```

Fields you leave out get the kind's default. The engine draws the annotation's appearance for
you, so it shows the same in every PDF app.

Stamps and file attachments also carry bytes (an image, a file). Pass those in a second
argument; see [Stamps](/docs/engine/annotations/stamps) and
[File attachments](/docs/engine/annotations/file-attachments).

## Change an annotation

`update()` takes the `ref` and only the fields you want to change:

```ts
await page.annotations.update(annotation.ref, {
  color: '#dc143c',
  contents: 'Reviewed',
});
```

A field you leave out keeps its value. A field set to `null` is removed, for example
`{ interiorColor: null }` for no fill. A field's value is replaced whole (a `file`, a list of
points), except rich text's `body` style, which merges.

You can also pass an annotation you read straight back: every field it has is one `update()`
accepts. That makes "change a copy, then save it" code simple:

```ts
await page.annotations.update(annotation.ref, { ...annotation, opacity: 0.5 });
```

Fields the engine writes (`author`, the dates, `userId`, `ref`…) are accepted and ignored. For
free text, send `contents` or `richText`, not both with different text.

### Move or resize by `rect`

An update takes a new `rect` for every annotation, as a script's `annot.rect` does in Acrobat. A
note, a file, a link, a popup and a redaction take it as it is.

Every other annotation is drawn from its shape: rectangles, ellipses, text boxes, stamps, carets
and form fields' widgets from their `box`, lines, polylines, polygons and ink from their points,
and text markup from its quads. For these, a new `rect` puts the shape there: each point is mapped from the old
`rect` onto the new one, each axis on its own. A `rect` of the same size moves the drawing, and a
bigger one stretches it:

```ts
const { rect } = line;

// 40 points right, 20 down
await page.annotations.update(line.ref, {
  rect: { ...rect, x: rect.x + 40, y: rect.y + 20 },
});
```

The engine then works out `rect` again from the shape. After a resize it can differ a little from
the one you sent: a line keeps its width, and its endings keep their size. The `rect` you read,
sent back, changes nothing, so a read passed straight back works.

- A new `rect` with a new shape (`box`, points or quads) fails with `InvalidArg`: send one or the
  other.
- A turned drawing can only move. A `rect` of another size fails with `InvalidArg`; resize its
  `box` or its points instead. A box at a quarter turn is the exception: it stands upright on the
  page, so a `rect` resizes it along its own sides.
- A `rect` that would squash a drawing to no width or no height fails with `InvalidArg`.

`shapeForRect(annotation, rect)` gives the fields that put an annotation at a `rect`, by the same
rule. Use it to place a copy somewhere else:

```ts
import { shapeForRect } from '@embedpdf/engine';

await page.annotations.create({ ...copy, ...shapeForRect(copy, target) });
```

A create works out a drawn annotation's `rect` from its shape, whatever `rect` you send, so a
copy of another app's drawing keeps its shape exactly. A box turned a quarter can be created by
its `rect` instead of its `box`; see [Turned shapes](/docs/engine/annotations/shapes#turned-shapes).

## Delete and reorder

```ts
await page.annotations.delete(annotation.ref);

await page.annotations.reorder([annotation.ref], 'start'); // to the back
await page.annotations.reorder([annotation.ref], { after: other.ref }); // just above another
```

Deleting an annotation also deletes its popup, if it has one, and its replies; see
[Delete a note](/docs/engine/annotations/notes-and-replies#delete-a-note).

Annotations on a page are drawn in order, so the last one is on top. `reorder(refs, position)`
moves annotations together, in the order you pass them, right after or before another annotation
of the page, or to the back (`'start'`) or the front (`'end'`). It resolves the page's new order,
`order`, as refs, and fires `annotations.reordered`.

Widgets are drawn above every annotation, in an order of their own, so a widget is never a
neighbour here: `reorder()` refuses one with `InvalidArg`. Restack widgets with
[`doc.forms.reorderWidgets()`](/docs/engine/forms/authoring#move-and-style-widgets).

## Style

Most kinds share the same style fields:

| Field           | What it is                                              | Default   |
| :-------------- | :------------------------------------------------------ | :-------- |
| `color`         | Line color, such as `'#1e90ff'`                         | per kind  |
| `interiorColor` | Fill color of closed shapes, or `null` for no fill      | no fill   |
| `opacity`       | From `0` (invisible) to `1` (solid)                     | `1`       |
| `strokeWidth`   | Line width in points                                    | `1`       |
| `borderStyle`   | `'solid'` or `'dashed'`                                 | `'solid'` |
| `dashArray`     | The pattern a `'dashed'` border draws, such as `[3, 2]` | `[3]`     |

A pattern alone doesn't make a border dashed: a `'solid'` border keeps its `dashArray` and draws
solid. `dashArray: null` on a dashed border draws the default `[3]`, 3 points on and 3 off. A
border from another app can read `'beveled'` or `'inset'`; only form widgets draw those, so a
write takes them only sent back unchanged.

Colors are `'#rrggbb'`, as in CSS: the engine gives them in lowercase and takes either case. They
never include transparency: that's what `opacity` is for. The default color depends on the kind
(yellow for a highlight, red for a shape, black for free text); each kind's page lists its
defaults.

A PDF can store a color as gray or CMYK. It reads as the same color in `'#rrggbb'`, and sent back
unchanged it stays as the file has it, so passing a read straight back rewrites nothing.

## Who wrote it

The engine records who created and last changed each annotation, from the user the document was
opened for. You read these fields; you don't set them:

| Field        | What it is                                          |
| :----------- | :-------------------------------------------------- |
| `author`     | The display name shown in PDF apps.                 |
| `createdAt`  | When it was created, as an ISO date.                |
| `modifiedAt` | When it was last changed.                           |
| `userId`     | The id of the user who owns it.                     |
| `createdBy`  | The id of the user who created it.                  |
| `modifiedBy` | The id of the user who last changed it.             |
| `groupId`    | The group it belongs to, when you work with groups. |

<Engine local>

The user comes from the `identity` you pass when you open the document; see
[Opening documents](/docs/engine/documents/opening#who-is-working-on-it).

</Engine>

<Engine cloud>

The user comes from the identity in their token; see
[Authentication](/docs/engine/authentication#what-a-token-carries).

</Engine>

## Flags

Every annotation has the standard PDF flags as `true`/`false` fields. Set the ones you need; the
rest stay off (on create) or keep their value (on update). The exception is `print`: a new
annotation prints unless you set `print: false`, as in Acrobat. A popup doesn't print.

```ts
await page.annotations.update(annotation.ref, { print: true, locked: true });
```

| Flag             | What it does                                            |
| :--------------- | :------------------------------------------------------ |
| `print`          | Printed with the page. Off: shown on screen only.       |
| `hidden`         | Not shown and not printed.                              |
| `noView`         | Not shown on screen (can still print).                  |
| `locked`         | Can't be moved, resized or deleted in PDF apps.         |
| `lockedContents` | Its text can't be changed in PDF apps.                  |
| `readOnly`       | Can't be changed at all in PDF apps.                    |
| `noZoom`         | Keeps its size when the page is zoomed (notes, icons).  |
| `noRotate`       | Stays upright when the page is rotated.                 |
| `invisible`      | Hidden when the app doesn't know the annotation's kind. |
| `toggleNoView`   | Flips `noView` when the pointer is over it.             |

Flags are instructions for PDF apps. The engine itself still lets you change a locked
annotation; the viewer's annotation tools respect the flags in their interface.

## Permissions

Reading annotations needs `doc.annotate.read`. Creating, changing and deleting them needs a
permission for that action, which can be limited to the user's own annotations:
`annotations:update:self` lets a user edit what they created and nothing else. See
[Permissions](/docs/engine/concepts/permissions#annotations).

<Engine local>

<CloudPdfCallout
  title="Let many people annotate one document"
  href="https://www.cloudpdf.com/docs/engine"
  cta="Collaborate with CloudPDF"
>

With CloudPDF, annotations are stored on your server as people make them. Everyone with the
document open gets each other's changes as [events](/docs/engine/concepts/events), and the server
makes sure people only change the annotations they may change.

</CloudPdfCallout>

</Engine>

## More

- [Import & export](/docs/engine/annotations/import-export): copy annotations to another
  document, or store them apart from the PDF.
- [Drawing annotations yourself](/docs/engine/annotations/appearances): render annotations as
  separate images, bake them into the page, or export them as a PDF.
- [Refs & identity](/docs/engine/concepts/identity): what a `ref` contains, and why you never
  build one.
- [Changes & undo](/docs/engine/documents/changes): make several annotation edits as one action,
  and undo them.

---

### Content Engine Forms Index (`docs/content/engine/forms/index.mdx`)

---
title: Forms
description: Read a PDF form's fields and values, fill them in, reset them, and move fields and values between documents.
---

# Forms

A PDF form is a set of fields: text boxes, checkboxes, radio buttons, dropdowns and lists. The
engine reads every field with its value, fills fields in, and redraws them so the new value shows
in every PDF app.

```ts
const { fields } = await doc.forms.list();

const name = fields.find((field) => field.name === 'billing.name')!;
await doc.forms.setValue(name.ref, { value: 'Ada Lovelace' });
```

## Read the form

`list()` returns every field in the document:

```ts
const { fields } = await doc.forms.list();

for (const field of fields) {
  console.log(field.name, field.family);
}
```

Every field has these:

| Field           | What it is                                                                                                                          |
| :-------------- | :---------------------------------------------------------------------------------------------------------------------------------- |
| `name`          | The full name, with the groups it's in: `billing.name`.                                                                             |
| `family`        | `'text'`, `'checkbox'`, `'radio'`, `'combobox'`, `'listbox'`, `'pushbutton'`, `'signature'` or `'unknown'`.                         |
| `ref`           | Its address. Keep it: it's how you fill, reset or change the field.                                                                 |
| `readOnly`      | `true` when PDF apps don't let the user change the value.                                                                           |
| `required`      | `true` when PDF apps ask for a value before the form is submitted.                                                                  |
| `noExport`      | `true` when the field is left out when the form is submitted.                                                                       |
| `alternateName` | The tooltip PDF apps show, also read by screen readers, or `null`.                                                                  |
| `groupId`       | The group the field belongs to: who fills it in, such as `'buyer'`, or `null`. See [Signers and groups](/docs/engine/forms/groups). |
| `widgets`       | Where the field shows on the pages (see [Widgets](#widgets)).                                                                       |

A document without a form returns an empty `fields`. The list's `formKind` says what the document
has: `'none'`, `'acroform'` (a regular PDF form) or `'xfa'` (see [Good to know](#xfa-forms)).

The list also has `needsAppearances`, `true` when the form asks PDF apps to draw its fields
themselves ([`repair()`](/docs/engine/forms/authoring#repair-a-broken-form) can clear it), and
`calculationOrder` (see [Calculations](#calculations-formatting-and-validation)).

### Values

Each family has its own value fields. Check `family` first, and TypeScript knows which ones the
field has:

```ts
if (field.family === 'text') console.log(field.value); // string
if (field.family === 'checkbox') console.log(field.checked); // boolean
if (field.family === 'listbox') console.log(field.selectedValues); // string[]
```

| Family       | Its value                                                          | Also                                                                                             |
| :----------- | :----------------------------------------------------------------- | :----------------------------------------------------------------------------------------------- |
| `text`       | `value`, a string                                                  | `defaultValue`, `maxLength` (`null`: no limit), `multiline`, `password`, `comb`                  |
| `checkbox`   | `checked`                                                          | `exportValue`: what the form data holds while it's checked                                       |
| `radio`      | `value`: the export value of the chosen button, or `'Off'`         | `noToggleToOff`, `radiosInUnison`                                                                |
| `combobox`   | `value`                                                            | `options`, `edit` (free text allowed), `defaultValue`                                            |
| `listbox`    | `selectedValues`, in option order                                  | `options`, `multiSelect`, `defaultValue` (option values), `topIndex` (the option in its top row) |
| `pushbutton` | none: a button only runs an action                                 | Each widget's `caption`, on its row in `widgets`                                                 |
| `signature`  | none here: see [Digital signatures](/docs/engine/forms/signatures) |                                                                                                  |

Each option of a dropdown (`combobox`) or list is `{ label, value, selected }`. `label` is what the
user sees; `value` is what the form stores, and what you pass when you fill the field in.

### Who created and filled it in

A field records who created it and who last filled it in, from the identity the document was opened
with:

```ts
field.createdBy; // 'user-17': who created the field, or null
field.filledBy; // 'user-42': whose fill last changed the value
field.filledByName; // 'Ann de Vries'
field.filledAt; // '2026-10-08T10:42:00Z'
```

| Field                                  | What it is                                                                                                                                                                            |
| :------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `createdBy`, `createdAt`               | Who created the field, and when. `null` for a field another app made.                                                                                                                 |
| `filledBy`, `filledByName`, `filledAt` | The user whose fill last changed the value, their name, and when. A calculated value counts as filled in by whoever's fill calculated it. `null` after a reset, or an anonymous fill. |
| `importedBy`                           | Who imported the field with its attribution kept.                                                                                                                                     |

The engine stamps these; a write can't set them. A fill that leaves the value as it was changes
none of them, and undoing a fill puts the previous ones back. A widget has no attribution of its own:
its field's is the one.

### Widgets

A field shows on a page through a **widget**: the box you see and click. Most fields have one. A
radio group is one field with a widget per button, and a field repeated on every page (a
reference number in the header, say) has a widget per page, all showing the same value.

Each widget in `field.widgets` has its `page` and its `ref`. The form also lists every widget in
`widgets`: where it is (`rect`, in points from the page's top-left), how it looks, its state,
what it does when clicked (`actions`), and the ref of the field it belongs to (`field`). So one read is enough to draw your own form layer
over a page:

```ts
const { widgets } = await doc.forms.list();

for (const widget of widgets) drawBox(widget.page, widget.rect, widget.field);
```

Checkbox and radio widgets in `field.widgets` also have their `exportValue` (what the form data
holds when this widget is checked) and `checked`.

A form field's widgets come with the form, never with the annotations: `page.annotations.list()`
doesn't return them, and reading them takes `doc.forms.read`, not `doc.annotate.read`. Each page's
widget images, every look and state, come from `page.forms.renderAppearances()`.

## Find a field

The `ref` from `list()` is the best address: it keeps working when the field is renamed. When you
know a field by its name instead (from your backend, or from data you stored), make a ref from the name:

```ts
import { toFieldRef } from '@embedpdf/engine';

const field = await doc.forms.get(toFieldRef('billing.name'));
```

The name is the full name, with dots between the groups. A field that isn't there fails with
`NotFound`.

## Fill in a field

`setValue()` takes the field's `ref` and its new value, in the same fields a read returns. What
you read, you can write back:

```ts
await doc.forms.setValue(name.ref, { value: 'Ada Lovelace' }); // text
await doc.forms.setValue(agree.ref, { checked: true }); // checkbox
await doc.forms.setValue(plan.ref, { value: 'yearly' }); // radio group
await doc.forms.setValue(country.ref, { value: 'NL' }); // dropdown
await doc.forms.setValue(toppings.ref, { selectedValues: ['cheese', 'olives'] }); // list
```

| Family     | Value                                                                           |
| :--------- | :------------------------------------------------------------------------------ |
| `text`     | `{ value }`; `null` empties it                                                  |
| `checkbox` | `{ checked }`                                                                   |
| `radio`    | `{ value }`: the export value of the button to choose; `'Off'` or `null` clears |
| `combobox` | `{ value }`: an option value; `null` clears                                     |
| `listbox`  | `{ selectedValues }`: option values; `[]` clears                                |

`setValue()` returns the field as it is now:

```ts
const { field } = await doc.forms.setValue(name.ref, { value: 'Ada' });
```

The engine redraws the field's widgets, so the page shows the new value in every PDF app. Render
the page again to see it.

### What the engine checks

The engine checks a value before it writes anything. A call that fails with `InvalidArg` leaves
the field as it was:

- The value must be the family's own: `{ checked: true }` on a text field fails.
- A dropdown or list takes option values, not labels. A dropdown with `edit: true` also takes
  free text. More than one value needs a list with `multiSelect`.
- A radio group's `value` must be the export value of one of its buttons.
- A radio group with `noToggleToOff` can't be cleared once a button is chosen.
- Push buttons and signature fields hold no value you can set.

Some things are fixed up rather than refused: text longer than `maxLength` is cut to fit, and
in a radio group with `radiosInUnison`, the buttons that share an export value check together.

`readOnly` is an instruction for PDF apps: they don't let the user change the field. The engine
still accepts your own writes, for example a total your code calculates.

## Reset

`reset()` puts back each field's `defaultValue`, or empties it when it has none. Without an
argument it resets the whole form; pass a ref, or a list of refs, to reset only those fields:

```ts
await doc.forms.reset(); // every field
await doc.forms.reset(name.ref);
await doc.forms.reset([name.ref, email.ref]);
```

It returns the fields it changed, as they are now. A field that already holds its default is
left alone and isn't in the list:

```ts
const { fields } = await doc.forms.reset();

console.log(`${fields.length} fields reset`);
```

Fields that hold no value, such as push buttons and signature fields, are skipped. Resetting the
whole form also skips the fields a signature locked and [fields stored inline](#fields-stored-inline);
name one of those yourself and the call fails, the same as a `setValue()` would.

## Move fields and values between documents

`export()` takes fields out as one bundle, each with its value. `importValues()` fills another
document's fields of the same name from it, and `import()` copies the fields themselves:

```ts
const bundle = await doc.forms.export();

const { fields, dropped } = await other.forms.importValues(bundle);
```

Each value is checked like a `setValue()`. A field the document doesn't have, a value it can't
take, or a field a signature locked is left out and listed in `dropped`; the rest still land, as
one change you can undo. See [Import & export](/docs/engine/forms/import-export).

## Events

`setValue()`, `reset()` and `importValues()` fire `forms.valueSet`, one per field that changed.
Each carries what the call returned for that field:

```ts
doc.events.on('forms.valueSet', ({ field }) => {
  updateSummary(field);
});
```

When one call changes several fields, such as `reset()` or `importValues()`, their events share
`origin.tx`, like an annotation import.

<Engine cloud>

Each value is saved on the server as you set it. Everyone with the document open gets the event,
with `origin.kind: 'remote'` for other people's changes. See
[Events](/docs/engine/concepts/events).

</Engine>

## Permissions

Reading the form needs `doc.forms.read`, and exporting it `doc.download` too. Filling in,
resetting and importing values needs `doc.forms.fill`, which includes `doc.forms.read`. See
[Permissions](/docs/engine/concepts/permissions).

A signed document can take filling away, for the whole form or for single fields. The call then
fails with `ProtectedDocument`; see
[Digital signatures](/docs/engine/forms/signatures#protection-what-signatures-allow).

## More

- [Creating form fields](/docs/engine/forms/authoring): add fields and widgets, change and delete
  them, repair a broken form.
- [Import & export](/docs/engine/forms/import-export): copy fields from one document to another,
  or fill a form in from a bundle.
- [Digital signatures](/docs/engine/forms/signatures): sign a signature field and check the
  signatures in a document.
- [Changes & undo](/docs/engine/documents/changes): fill in several fields as one action, and
  undo it.

## Good to know

### Calculations, formatting and validation

The engine doesn't run the form's JavaScript. `setValue()` stores the value you give it: it
doesn't recalculate a total, format a date or an amount, or run a validation script. PDF apps run
those scripts while a user types.

A field's scripts are in `field.actions` (`keystroke`, `format`, `validate` and `calculate`), and
the list's `calculationOrder` holds the fields whose totals are calculated, in order. To write them,
see [Scripts and calculations](/docs/engine/forms/authoring#scripts-and-calculations).

The viewer's form plugin runs these scripts and writes their results as one
[change](/docs/engine/documents/changes): `forms.setValue`, `forms.reset`, `forms.setDisplay`
(whether the field shows and prints) and `forms.setAppearanceText` (the formatted text the field
shows, while its value stays the same) ops, in the order the scripts asked for them. All of them
are written, or none is: a field a signature locked refuses the whole change. You only need these
ops when you run form scripts yourself.

### Recovered fields

Some PDFs have fields that show on the page but are missing from the form's list of fields, a
bug in the tool that made them. The engine finds them anyway: they come back with
`origin: 'recovered'` and fill in like any other field. Other PDF apps may not see them;
[`repair()`](/docs/engine/forms/authoring#repair-a-broken-form) fixes the file.

### A field on several pages

A field's widgets can be on different pages. The `meta` of a `setValue()` or `reset()` result
lists every widget whose look changed, with its page (`meta.changedWidgets`). For a radio group
that's the button switched off and the one switched on. Use it to render only the pages that
changed.

`meta.redrawnWidgets` lists those whose picture the engine drew again, such as a text field's
widgets after a new value. A checkbox or radio button that switches shows another picture it
already has, so it isn't in the list.

### Checkboxes with several widgets

A checkbox can have several widgets, each with its own `exportValue`: a "Yes / No" pair, for
example. `{ checked: true }` checks the first widget; `{ value: 'No' }` checks the widget with
that export value.

In the file, each checkbox and radio widget also has an `onState`: the name of its checked look
(PDF's appearance state). It's usually the same as the export value, and widgets the engine
creates always use their `exportValue` for it. You never need it to fill a field in.

The field's value in the file (`valueEntry`) is the checked widget's `onState`, or `'Off'`, as the
PDF standard says. Some tools write the widget's position instead (`'0'`, `'1'`); such a value
still resets to that widget.

### XFA forms

`formKind` is `'xfa'` when the PDF also carries an XFA form, an older XML form format. The
engine works with the document's regular fields and never runs XFA.

### Fields stored inline

A field that the file stores inside another object, rather than as an object of its own, can be
read but not changed: writes fail with `InvalidArg`. PDF tools rarely write fields this way.

---

### Content Headless Annotations Index (`docs/content/headless/annotations/index.mdx`)

---
title: Annotations
description: Show the annotations in a PDF, let people draw new ones, and work with them from code.
---

import { Cards, Card } from '@/components/docs/cards';

{/* This page is served at `…/headless/<framework>/annotations` with NO trailing
    slash, so a relative href resolves against `…/headless/<framework>/`.
    Sibling pages are `annotations/tools`, other sections `text/selection`. */}

# Annotations

Annotations are what people add on top of a page: highlights, sticky notes, shapes, stamps and
text boxes. The annotation plugin shows the ones a PDF already has, lets people draw and edit new
ones, and gives your code the same abilities. Every annotation is saved into the PDF, so it looks
the same in every other PDF app.

## Show the annotations in a PDF

Register `interactionPlugin()` and `annotationPlugin()`, then put an `<AnnotationLayer>` above the
rendered page:

<Example name="annotation/layer" />

> **Try it:** click the rectangle and drag it, or a corner to resize it. Double-click the text box
> to type in it.

<Snippet name="annotations/index/layer" />

While the annotation layer is on the page, the render layer leaves the annotations out of the page
picture by itself. The annotation layer draws them instead, so they can be selected, moved and
changed.

`interactionPlugin()` is the shared input system. Every tool, from text selection to drawing,
speaks through it; see [Selecting & copying](text/selection) for how it works.

## Let people draw

A **tool** says what a click or a drag on the page creates. Activate one and people can draw:

<Example name="annotation/draw" />

> **Try it:** drag on the page to draw a rectangle, then pick another tool. **Highlight** marks the
> text you select.

<Snippet name="annotations/index/toolbar" />

- **`pointer`** selects: click an annotation to select it, then move, resize or turn it.
- **`square`** draws a rectangle: drag, or just click for a default-size one.
- **`ink`** draws freehand.
- **`highlight`** highlights the text you select.
- **`note`** drops a sticky note where you click.

After you draw, the new annotation is selected and the tool stays active, so you can draw another.
You can change both; see [What happens after drawing](annotations/tools#what-happens-after-drawing).

## How it fits together

Everything on these pages is one of five things:

| Thing          | What it is                                                                         | Where you use it                           |
| :------------- | :--------------------------------------------------------------------------------- | :----------------------------------------- |
| **Annotation** | A record in the PDF: its kind, position, colors and text.                          | `annotation.list()`                        |
| **Tool**       | What a click or drag creates, with which defaults, cursor and preview.             | `annotationPlugin({ tools })`              |
| **Layer**      | What each page draws: the annotations, the selection box and handles, the preview. | `<AnnotationLayer>`                        |
| **Overlay**    | Your own UI attached to an annotation, such as a menu or a hover card.             | `<AnnotationMenu>`                         |
| **API**        | Reading, creating, changing and deleting annotations, and listening for changes.   | `useAnnotation()`                          |

An annotation here is the same record the engine reads and writes: the same kinds, the same fields
and the same names. What you learn in the [engine's annotation pages](/docs/engine/annotations)
holds here too.

## The built-in tools

| Tool                                              | Makes                                       | How                                   |
| :------------------------------------------------ | :------------------------------------------ | :------------------------------------ |
| `square`, `circle`                                | A rectangle or an ellipse                   | Drag, or click for a default size     |
| `line`                                            | A line                                      | Drag, or click for a default length   |
| `polygon`, `polyline`                             | A closed or open shape of points            | Click each point, double-click to end |
| `ink`, `ink-highlight`                            | Freehand drawing, or a see-through marker   | Draw                                  |
| `free-text`, `free-text-callout`                  | A text box, or one with a line pointing out | Click or drag, then type              |
| `highlight`, `underline`, `strikeout`, `squiggly` | A mark over text                            | Select text                           |
| `insert-text`, `replace-text`                     | A suggested insertion or replacement        | Select text                           |
| `note`                                            | A sticky note                               | Click                                 |
| `stamp`                                           | An image or a PDF drawing                   | Click, then pick a file               |
| `attachment`                                      | A file pinned to a spot                     | Click, then pick a file               |
| `link`                                            | An area that opens a page or a website      | Drag                                  |

Measurement tools (`distance`, `perimeter`, `area`) and redaction (`redact`) are annotation tools
too. Their plugins add the rest of what they need.

You can change any tool, or add your own; see [Tools & defaults](annotations/tools).

## Who wrote it

Every annotation records who made it and who last changed it (`author`, `createdAt`,
`modifiedAt`). You don't set these on the annotation: they come from the user the document was
opened for.

<Engine local>

Tell the viewer who the user is, once. Every document it opens uses it:

<Example name="annotation/identity" />

> **Try it:** draw a rectangle. The list says Dana Smith made it, and when.

<Snippet name="annotations/index/identity" />

`displayName` becomes the `author` other PDF apps show, and `userId` records who owns the
annotation. The same identity decides which annotations count as the user's own when
[permissions](concepts/permissions) say "only their own".

To open one document as someone else, give its entry in `initialDocuments` its own `identity`. It
replaces the viewer's for that document:

```ts
const initialDocuments = [
  { source: contract, identity: { userId: 'bot', displayName: 'Import job' } },
];
```

A document keeps the identity it was opened with. <Fw only={['react', 'vue', 'svelte']}>If you change the viewer's `identity`, documents
you open afterwards use the new one.</Fw>

<Fw only="angular">

To change the viewer's identity, for example when someone else signs in, use
`inject(EpdfViewer).updateSettings({ identity })`. Documents you open afterwards use the new one. To
follow your app's login:

```ts
effect(() => viewer.updateSettings({ identity: auth.identity() }));
```

</Fw>

</Engine>

<Engine cloud>

It comes from the identity in the user's token; see
[Authentication](/docs/engine/authentication#what-a-token-carries).

</Engine>

## Settings

Set them when you register the plugin. `snap`, `chrome`, `afterCreate` and `editable` can change
while the app runs, with `annotation.updateSettings({ … })`; `annotation.resetSettings()` goes back to what you
registered, and `useAnnotationSettings()` gives you the current settings.

| Setting       | What it does                                                          | Page                                                        |
| :------------ | :-------------------------------------------------------------------- | :---------------------------------------------------------- |
| `tools`       | Change the built-in tools, or add your own                            | [Tools & defaults](annotations/tools)                       |
| `afterCreate` | Select a new annotation? Keep the tool active?                        | [What happens after drawing](annotations/tools#what-happens-after-drawing) |
| `chrome`      | The selection's outline, handles and rotation handle, and their colors | [Change the handles](annotations/selecting#change-the-handles) |
| `snap`        | Snapping to other annotations and to angles                           | [Snapping](annotations/selecting#snapping)                  |
| `editable`    | Which annotations people may change by hand: `true`, `false`, or kinds | [What people may change](annotations/selecting#what-people-may-change) |

Every color in `chrome` can also come from CSS, for dark mode or a different look per viewer: see
[Colors & theming](ui/theming).

## Next steps

<Cards>
  <Card
    title="Annotations in code"
    description="Read, create, change and delete annotations from your own code."
    href="annotations/with-code"
  />
  <Card
    title="Events, saving & sharing"
    description="Listen for changes, save them into the PDF, or move them to your own server."
    href="annotations/saving"
  />
  <Card
    title="Tools & defaults"
    description="Default colors, your own tools, the cursor, and a preview before placing."
    href="annotations/tools"
  />
  <Card
    title="Selecting & editing"
    description="Selection, a menu, a style panel, and your own handles."
    href="annotations/selecting"
  />
  <Card
    title="Your own annotation look"
    description="Draw an annotation with your own component, and show cards on hover."
    href="annotations/custom-rendering"
  />
  <Card
    title="Comments & replies"
    description="Build a comments sidebar with threads, replies and review states."
    href="annotations/comments"
  />
</Cards>

---

### Content Headless Forms Index (`docs/content/headless/forms/index.mdx`)

---
title: Filling forms
description: Let people fill in PDF forms, read and set values from your code, check required fields, and move form data in and out.
---

# Filling forms

Many PDFs are forms: text boxes, checkboxes, dropdowns, signature fields. Register `formPlugin()` and
put `<FormLayer>` in each page, and people can fill them in:

<Snippet name="forms/form-layer" />

<Example name="form/basic" />

Each field becomes a real HTML control, placed and styled like the field in the PDF: people type in text
boxes, tick checkboxes, pick from lists, and move between fields with Tab. A value is saved into the
document as it's entered, so it's there when the PDF is downloaded, and every PDF app shows it. The
form layer paints the fields as the engine draws them, so while it's on the page, the render layer
leaves them out of the page picture.

## Read the form

<Fw only={['react', 'vue', 'svelte']}>

`useFormState()` gives you every field, and <Word of="rerenders" /> when a value changes:

</Fw>

<Fw only="angular">

`form.fields()` is every field, as a signal: your view updates when a value changes.

</Fw>

<Snippet name="forms/field-list" />

Each field has its full `name` (such as `billing.name`), its `family` (`'text'`, `'checkbox'`,
`'radio'`, `'combobox'`, `'listbox'`, `'pushbutton'` or `'signature'`), its value, and a `ref`: its
address for everything else. The fields and their values are the engine's; see
[Forms](/docs/engine/forms#read-the-form) for every family's value.

For one field, such as a total you show in your own UI, use `useFormValue()`:

<Snippet name="forms/invoice-total" />

`toFieldRef(name)` makes a ref from a field's full name, for when you know a field by name.

<Example name="form/read" />

## Fill in a field

`setValue()` takes the same values a field reads:

```ts
await form.setValue(name.ref, { value: 'Ada Lovelace' }); // text
await form.setValue(agree.ref, { checked: true }); // checkbox
await form.setValue(plan.ref, { value: 'yearly' }); // radio group: the button's export value
await form.setValue(country.ref, { value: 'NL' }); // dropdown: an option's value
await form.setValue(toppings.ref, { selectedValues: ['cheese', 'olives'] }); // list
```

It runs the same checks and scripts as typing does, and resolves with `{ field, status }`: `status`
is `'applied'`, `'unchanged'`, or `'rejected'` when the form's own validation refused the value.

The field shows the new value at once, before the engine answers, and the promise resolves once it
has. What the form's scripts calculate from it goes with it, as one change: one request, and one
step to undo. The field stays editable meanwhile; filling it in again is the next change. A value
the engine refuses goes back to the one the field had.

<Fw only="angular">

Your own inputs can fill in fields too. `form.controlOf(ref)` gives you a field as an Angular
`FormControl`: it holds the field's value, and fills in the field when the control changes, with
the same checks and scripts:

<Snippet name="forms/form-control" />

</Fw>

To fill many fields at once, such as from your backend, pass them by name:

```ts
await form.importValues({ 'billing.name': 'Ada Lovelace', 'billing.country': 'NL', agree: true });
```

`importValues()` and `setValues()` write every value as one change, with the calculations run once
after them. Each field's own keystroke and validation scripts don't run, as when a script sets a
field's `value` in Acrobat.

<Example name="form/fill" />

## Check required fields

A form marks some fields as required. Check them before you submit or download:

```ts
const { valid, missing } = form.validate();

const widget = !valid && form.getWidget(missing[0].widgets[0]);
if (widget) stage.reveal(widget.page, { rect: widget.rect });
```

`missing` lists the required fields that are empty, in page order, so you can take the user to the
first one. Only the fields the user may fill in count: another signer's are theirs to fill.

<Example name="form/validate" />

## Calculations, formatting and validation

Many forms add up totals, format dates and check values with scripts. With JavaScript on in the
[actions plugin](documents/actions), they run as people type, as in Acrobat:

<Snippet name="forms/scripts" />

When a script refuses a value, such as a date in the wrong format, the field keeps its old value,
the box keeps what was typed so it can be corrected, as in Acrobat, and
<Fw only={['react', 'vue', 'svelte']}>`onValidationRejected`</Fw><Fw only="angular">`validationRejected$`</Fw> tells you why, so you can show a message.

The scripts read the form as the user sees it, values still on their way included, so nothing is
read from the engine for them. Their engine starts when the first field of a form with scripts takes
focus, so the first value doesn't wait for it; a form without scripts never starts it.

## Reset the form

```ts
await form.reset(); // every field the user may fill in, back to its default
await form.reset([name.ref, email.ref]); // some fields
```

## Move form data in and out

For your own backend, plain values are simplest:

```ts
const values = form.exportValues(); // { 'billing.name': 'Ada Lovelace', agree: true, … }
await form.importValues(values);
```

Fields are matched by their full name. A field the form doesn't have, or a value it can't take, is
skipped; the rest still land.

To copy the fields themselves to another document, with their boxes, values and scripts, take
them out as a bundle and import it there. `FormTransfer` turns a bundle into one JSON text to
store, and back:

```ts
const bundle = await form.export(); // every field; or form.export({ fields }), ({ pages })
const text = FormTransfer.stringify(bundle);

const { fields, dropped } = await otherForm.import(await FormTransfer.parse(text));
```

The import adds the fields as one change you can undo. A field whose name the form has already is
left out and listed in `dropped`. By default each field keeps who created and filled it in, which
needs `canRestoreAttribution()`; pass `{ attribution: 'stamp' }` to make them yours. See
[Import & export](/docs/engine/forms/import-export).

<Example name="form/data" />

## Methods

What `useForm()` gives you for reading and filling. The verbs that change the form itself are
on [Building forms](forms/building#methods).

| Method                     | What it does                                                                                                                                   |
| :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- |
| `get(ref)`                 | One field, with its value, or `null` when the form has no such field. [Read the form](#read-the-form)                                           |
| `getValue(ref)`            | One field's value, in the shape `setValue()` takes, or `null`. In a component, use `useFormValue(ref)`.                                        |
| `getFieldForWidget(widget)` | The field a box belongs to, from the box's ref                                                                                                |
| `getWidget(widget)`        | A box's row: its page, its place (`rect`), its look and its state, or `null` when no page shows it                                             |
| `getWidgets(ref)`          | Every box a field shows in, as rows in the field's order: a radio group's buttons, or a field repeated on several pages                       |
| `listWidgets(page)`        | Each field box on a page, with its place, its control, its value and whether it can be filled. For drawing the fields yourself.                |
| `getWidgetAt(page, point)` | The field box under a point on a page, with its field, or `null`                                                                               |
| `setValue(ref, value)`     | Fills in a field with `{ value }`, `{ checked }` or `{ selectedValues }`, running the form's checks and scripts. Resolves `{ field, status }`. [Fill in a field](#fill-in-a-field) |
| `setValues(entries)`       | Fills in several fields, each `{ ref, value }`, as one change with one recalculation; a field you may not fill in, or a value it can't take, fails on its own. Resolves `{ applied, skipped, failed }`. |
| `importValues(values)`     | Fills in fields by full name from a plain object, as one change with one recalculation. Resolves `{ applied, skipped, failed }`. [Move form data in and out](#move-form-data-in-and-out) |
| `exportValues()`           | Every value, keyed by full name, as a plain object                                                                                             |
| `export(selection?)`       | Whole fields with their boxes, values and scripts, as a bundle: every field, or `{ fields }` or `{ pages }`. [Move form data in and out](#move-form-data-in-and-out) |
| `import(bundle, options?)` | Adds a bundle's fields, as one change. Resolves `{ fields, refMap, dropped }`. Needs `canDesign()`. [Move form data in and out](#move-form-data-in-and-out) |
| `reset(refs?)`             | Puts back each field's default, for every field the user may fill in or the fields you pass. Resolves `{ fields }`, the fields it changed. [Reset the form](#reset-the-form) |
| `validate()`               | Checks the required fields the user may fill in. Returns `{ valid, missing }`, the empty ones in page order. [Check required fields](#check-required-fields) |
| `activateWidget(widget)`   | Does what a click on a field box does, such as running a button's action                                                                       |
| `getSnapshot()`            | Everything at once, as the user sees it: what the engine's `list()` returns, with the values still on their way. `fields`, `widgets`, `formKind`, `needsAppearances` and `calculationOrder` |
| `refresh()`                | Reads the whole form again, and <Fw only={['react', 'vue', 'svelte']}>`onResynced`</Fw><Fw only="angular">`resynced$`</Fw> fires. You rarely need it: the plugin follows every change, whoever made it.                      |
| `canRead()`, `canFill(ref)` | Whether reading the form, and filling in a field, are allowed. [Permissions](#permissions)                                                   |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()` | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use `useFormSettings()`. [Settings](#settings) |

Every method that returns a promise also takes a `signal` in its options, to cancel it.

## State

<StateIntro hook="useFormState()" />

| Field           | What it is                                                                                      | Read once                                            |
| :-------------- | :---------------------------------------------------------------------------------------------- | :--------------------------------------------------- |
| `fields`        | Every field, with its value                                                                     | `list()`, or `list({ family, page, name })` for some |
| `status`        | Whether the form is read: `'idle'`, `'loading'`, `'ready'`, `'forbidden'` or `'error'`           | `getStatus()`                                        |
| `formKind`      | What the document has: `'none'`, `'acroform'` (a regular PDF form) or `'xfa'`                   | `getFormKind()`                                      |
| `selectedField` | In design mode, the field of the selected box, or `null`; see [Building forms](forms/building#change-a-fields-settings) | `getSelectedField()`          |

For one field, `useFormValue(ref)` <Word of="rerenders" /> only when that field's value changes.

## Settings

Set them when you register the plugin, and change them while the app runs with
`form.updateSettings({ … })`; `form.resetSettings()` goes back to what you registered, and
`useFormSettings()` gives you the current settings.

| Setting             | What it does                                                               | Default                      | From CSS                       |
| :------------------ | :------------------------------------------------------------------------- | :--------------------------- | :----------------------------- |
| `validation`        | Run the form's own checks (`'scripts'`), or accept every value (`'none'`)  | `'scripts'`                  | —                              |
| `focus.color`       | The ring around the field being filled in                                  | the viewer's accent          | `--epdf-form-focus`            |
| `fields.border`     | The edge of a field that has no border of its own                          | the viewer's accent at 55%   | `--epdf-form-field-border`     |
| `fields.background` | Inside a field that has no background of its own                           | `'rgb(255 255 255 / 0.92)'`    | `--epdf-form-field-background` |
| `fields.color`      | The check mark and text of a field that has no color of its own            | `'#1f2a44'`                    | `--epdf-form-field-color`      |
| `fillable`          | Which fields people fill in by hand: `true`, `false`, or field families    | `true`                       | —                              |

Scripts need JavaScript on in the actions plugin; without it, `'scripts'` accepts every value too,
and a form with scripts logs a hint once.

`fillable` is for the people using your app. A field it leaves out draws as a read-only field
does, and takes no typing, clicks or picks; your code still fills it in with `setValue()` and
`importValues()`, and `canFill(ref)` doesn't change:

```ts
formPlugin({ fillable: false }); // show the values, take no input
form.updateSettings({ fillable: ['text', 'checkbox'] }); // only these families
form.updateSettings({ fillable: true }); // every field again
```

The families are `'text'`, `'checkbox'`, `'radio'`, `'combobox'`, `'listbox'`, `'pushbutton'` and
`'signature'` (signing a field is filling it in). To stop your code too, leave `doc.forms.fill` out
of the scope (see [Permissions](#permissions)).

The colors are what the viewer draws around fields; a field's own colors are saved in the PDF (see
[Building forms](forms/building)). A CSS variable wins over the setting: see
[Colors & theming](ui/theming).

<Example name="form/colors" />

## Events

<Snippet name="forms/autosave" />

| Event                  | Fires when                                                                | Carries           |
| :--------------------- | :------------------------------------------------------------------------ | :---------------- |
| `onValueChanged`       | A field's value changed                                                   | `field`, `origin` |
| `onValidationRejected` | The form's validation refused a value                                     | `field`, `issues` |
| `onResynced`           | The whole form was read, as when it opens, after `refresh()` or a repair  | `snapshot`        |
| `onSettingsChanged` | The settings changed | `settings`, `changed` |

Fields that are added, changed or removed have their own events; see
[Building forms](forms/building#events).

## Permissions

| Permission       | What it allows   | Check               |
| :--------------- | :--------------- | :------------------ |
| `doc.forms.read` | Reading the form | `form.canRead()`    |
| `doc.forms.fill` | Filling it in    | `form.canFill(ref)` |

Without it, the fields show their values but can't be changed. A user with a `fields:fill`
permission for one group fills in that group's fields only: `form.canFill(ref)` answers per field,
the other fields can't be changed and Tab skips them, `validate()` leaves them out, and `reset()`
leaves them as they are. See [Signers and groups](/docs/engine/forms/groups).

## Good to know

### Forms and annotations

A field's widgets are the form's: the annotation plugin never lists them. With the
[annotation plugin](annotations) registered, the `<FormLayer>` and the annotation layer share the page: fields stay fillable with the
`pointer` tool, and become movable boxes in [design mode](forms/building).

### Keyboard

Tab moves to the next field in the page's order. In a text box, Enter confirms and Escape puts back
the value it had.

### Collecting responses

<Engine local>

On the local engine, the values live in the document in this tab until it's downloaded.

<CloudPdfCallout
  title="Collect form responses on your server"
  href="https://www.cloudpdf.com/docs/headless/forms"
  cta="Collect forms with CloudPDF"
>

With CloudPDF, each value is stored on your server as it's entered, so a half-filled form survives a
closed tab, and your backend reads the answers without asking the user to download or upload a
file.

</CloudPdfCallout>

</Engine>

<Engine cloud>

Each value is stored on your server as it's entered, so a half-filled form survives a closed tab.

</Engine>

### XFA forms

Some PDFs also carry a form in the older XFA format. The viewer works with the document's regular
fields and never runs XFA; see [Forms](/docs/engine/forms#xfa-forms).

### Signed documents

A signature can lock the whole form, or some fields. Locked fields show their values and can't be
changed; see [Signatures](forms/signatures).

---

### Content Viewer Features Index (`docs/content/viewer/features/index.mdx`)

---
title: All features
description: Everything the viewer can do, how to leave a feature out, who the user is, and where each feature's settings go.
---

# All features

Every feature is on from the start. Leave out the ones you don't need with `disable`, by the name
in the first column:

```js
{ disable: ['forms', 'redaction', 'measurement'] }
```

| Feature | What people can do | Settings |
| :-- | :-- | :-- |
| `annotations` | Highlight, underline and strike out text; draw; add notes, text boxes and callouts | [Annotations](features/annotations) |
| `shapes` | Draw rectangles, circles, lines, arrows and polygons | [Annotations](features/annotations#the-tools) |
| `comments` | Comment on annotations and reply, in a panel with every thread | [Annotations](features/annotations#comments) |
| `stamps` | Place stamps: the standard ones, their own, or images | [Annotations](features/annotations#stamps) |
| `signatures` | Draw, type or upload a signature; sign fields; check signatures | [Forms & signatures](features/forms-and-signatures#signatures) |
| `forms` | Fill in forms; build them in the Form mode | [Forms & signatures](features/forms-and-signatures) |
| `redaction` | Mark text and areas, then remove them from the file for good | [Annotations](features/annotations#redaction) |
| `measurement` | Measure distances, perimeters and areas at the drawing's scale | [Annotations](features/annotations#measurements) |
| `pages` | Rotate, move, insert, duplicate and delete pages in the thumbnails panel, each step undoable | [Panels](customize/panels#organize-pages) |
| `attachments` | See, save, attach and remove files inside the PDF | [Panels](customize/panels#the-attachments) |
| `search` | Find text, with every match highlighted and listed | [`plugins.search`](/docs/headless/text/search#settings) |
| `text` | Select and copy text | [`plugins.selection`](/docs/headless/text/selection#settings) |
| `thumbnails` | A picture of every page, to jump around | [Panels](customize/panels) |
| `outline` | The document's table of contents | [Panels](customize/panels) |
| `documents` | Open other files (with the local engine), download, print, several documents in tabs | [Documents](documents) |

## Leave features out

A feature in `disable` is gone everywhere at once: its buttons, its mode tab, its panel, its menu
rows and its shortcuts. Annotations already in the document still show; people just can't add new
ones. Most features have parts you can turn off on their own, such as `forms.building` or
`documents.download`, and one button goes by its command's name, such as `tool:highlight`.

`disable` follows its prop, so a feature can come and go while the viewer runs, for example when
a user's plan changes.

[Turn features off](features/turning-off) has every name, read-only, and what to use when: the
layout for where something goes, `disable` for whether the viewer has it, and the scope for what a
user may do.

## Who the user is

<Engine local>

The user's name becomes the author of the annotations and comments they make:

```js
{ identity: { displayName: 'Ada Lovelace' } }
```

</Engine>

<Engine cloud>

The user's name becomes the author of the annotations and comments they make. On CloudPDF, who the
user is and what they may do come from the document's token, so your server decides.

</Engine>

## Each feature's settings

Every feature is a headless plugin, and `plugins` takes its settings by the plugin's name, exactly
as the plugin's page lists them:

```js
{
  plugins: {
    stage: { spread: 'odd', zoom: { mode: 'fit-width' } },
    search: { highlight: { color: '#a7f3d0' } },
    annotation: { tools: [{ id: 'highlight', defaults: { color: '#fde047' } }] },
  },
}
```

That opens documents as two-page spreads at the page's width, with green search matches and
yellow highlights. See [Plugins](code/plugins) for every plugin and how to change its settings
while the viewer runs.

## Good to know

### What loads when

A feature you disable is still in the viewer's code: `disable` changes what people see, not what
you ship. The first load stays small another way. Each feature's panels and dialogs are files of
their own, which the viewer fetches in the background once the document shows, so they open at
once. The heavier parts wait until they're used: the signature checks until a signature is
checked, the standard stamps until Stamps opens, the handwriting fonts until someone types a
signature, and each language until it's chosen. [How it works](concepts/how-it-works#what-loads-when)
has the whole order.

---

### Website Src Content Docs Index (`website/src/content/docs/index.mdx`)

---
title: Documentation
description: Choose the ready-made viewer, headless components, or the EmbedPDF Engine directly.
searchable: false
---

<DocsOverview />

---

### Cloudpdf Website Src Content Docs Index (`cloudpdf/website/src/content/docs/index.mdx`)

---
title: Introduction
searchable: false
description: Documentation for CloudPDF — the cloud PDF viewer with collaboration, access control, and secure storage built in.
---

import { DocsLanding } from '@/components/site/docs-landing';

<DocsLanding />

---

### Website Src Content Docs Engine Index (`website/src/content/docs/engine/index.mdx`)

---
title: Engine
description: Open, read, change and save PDF files from JavaScript, with one typed API.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

import { Cards, Card } from '@/components/docs/cards';

# The PDF engine

<Engine local>

The EmbedPDF engine opens PDF files in the browser and lets you render their pages, read and
search their text, add annotations, fill forms, edit pages and save the result. It runs PDFium,
the PDF library inside Chrome, in a Web Worker, so your page stays responsive. Nothing is
uploaded: the file never leaves the device.

The ready-made viewer and the headless components are built on this engine. Use it directly
when you want PDF features without a viewer: a thumbnail job, a form filler, an export script.

</Engine>

<Engine cloud>

The CloudPDF engine gives your app a full PDF engine over HTTPS. Your files stay on your
CloudPDF server. The engine renders their pages, reads and searches their text, stores
annotations and form values, and checks every call against the permissions in the user's
token.

The CloudPDF viewer and the headless components are built on this engine. Use it directly when
you want PDF features without a viewer: a thumbnail job, a form filler, an export script.

</Engine>

<Engine local>

```ts
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
const doc = await engine.open({ kind: 'bytes', bytes });

const { pages } = await doc.pages.list();
const image = await doc.page(pages[0].ref).render.image();
```

</Engine>

<Engine cloud>

```ts
import { cloudEngine } from '@cloudpdf/engine';

const engine = cloudEngine();
const doc = await engine.open({ kind: 'token', token: () => fetchDocumentToken(documentId) });

const { pages } = await doc.pages.list();
const image = await doc.page(pages[0].ref).render.image();
```

</Engine>

## What you can do

- **[Render pages](/docs/engine/documents/rendering)** as images, at any size.
- **[Read](/docs/engine/text/extraction), [search](/docs/engine/text/search) and
  [select](/docs/engine/text/selection-and-geometry)** the text on a page, and find out where
  each character is.
- **[Add and edit annotations](/docs/engine/annotations)**: highlights, notes, shapes, stamps,
  measurements. Copy them to another document with
  [import & export](/docs/engine/annotations/import-export).
- **[Fill forms](/docs/engine/forms)**, create form fields and
  [sign](/docs/engine/forms/signatures) documents.
- **[Change pages](/docs/engine/documents/pages)**: rotate, reorder, delete, insert, merge and
  split.
- **[Redact](/docs/engine/documents/redaction)** content for good, and
  **[save](/docs/engine/documents/saving)** the result.
- **[Group edits into changes](/docs/engine/documents/changes)** that apply all or none, and undo
  or redo any of them.

Every call is async, can be cancelled, and fails with a typed error you can branch on. The same
code runs on the local engine and on the CloudPDF engine: only how you create the engine and
open a file differs.

A few things exist only on the local engine: its fonts, `warmup()`, raw pixels, and saving a
document's layer or to a file. A document opened with `localEngine()` has them. Code that takes
any engine checks first:

```ts
import { isLocalDocument } from '@embedpdf/engine';

if (isLocalDocument(doc)) {
  const layer = await doc.downloadLayer();
}
```

`isLocalEngine(engine)` does the same for an engine.

<Engine local>

## Local or cloud

This engine runs in the browser: no server, no upload, and it works offline. That makes it the
right choice when the user already has the file, or when the file may not leave their device.

<CloudPdfCallout
  title="Keep the file on your server"
  href="https://www.cloudpdf.com/docs/engine"
  cta="Read about the CloudPDF engine"
>

When the file lives on your server, the CloudPDF engine runs the same API there and checks every
call. A user sees the first page as an image, without downloading the whole PDF or the engine
(about 3 MB compressed), and a user who may not download the file never receives it. Your code
stays the same: swap `localEngine()` for `cloudEngine()`.

</CloudPdfCallout>

</Engine>

## Next steps

<Cards>
  <Card
    title="Quick start"
    description="Open a PDF, render a page, search it and add a highlight."
    href="/docs/engine/quick-start"
  />
  <Card
    title="Setup"
    description="Install the engine and create it once for your app."
    href="/docs/engine/setup"
  />
  <Card
    title="Annotations"
    description="Read, add and change annotations of every kind."
    href="/docs/engine/annotations"
  />
</Cards>

---

### Website Src Content Docs Headless Index (`website/src/content/docs/headless/index.mdx`)

---
title: Headless
description: Build your own PDF viewer from plugins, components and hooks, in React, Vue, Svelte or Angular.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

import { Cards, Card } from '@/components/docs/cards';

{/* Served at `…/headless/<framework>` with NO trailing slash: a relative href
    resolves against `…/headless/`, so pages are `quick-start`,
    `viewing/stage`, `concepts/how-it-works`. */}

# Build your own PDF viewer

The headless packages give you everything a PDF viewer does, without a look of their own: pages
that scroll and zoom, crisp rendering, text selection and search, annotations, forms and
signatures. You build the interface in <Framework />, and nothing appears on screen unless you put
it there.

<Engine local>

<Snippet name="start/overview" />

</Engine>

<Engine cloud>

<Snippet name="start/overview-cloud" />

</Engine>

That's a complete viewer: the pages scroll, zoom with a pinch or Ctrl and the wheel, and stay sharp
at every zoom. Everything else is a plugin you add, and a few lines of your own UI.

## Headless or the ready-made Viewer?

EmbedPDF comes in two forms, built on the same engine and plugins:

- **The [Viewer](/docs/viewer)** is finished: a toolbar, sidebars, annotation tools and search,
  ready in minutes. You configure it and theme it.
- **Headless** is the parts: you decide what the toolbar looks like, where the search box goes, and
  what happens when someone clicks an annotation.

Choose headless when the viewer is part of your product's own interface. You can start with the
Viewer and move to headless later: the plugins and their options are the same.

## How it's put together

A headless viewer has five parts. You meet them on every page:

- **The engine** opens and reads PDF files. It runs in the browser, or on your server with
  CloudPDF.
- **`<Viewer>`** starts the engine, opens documents, and holds the plugins.
- **Plugins** add one feature each: search, annotations, forms. Register the ones you need; the
  rest never reach your bundle.
- **Layers** draw inside each page: the page itself, the text selection, the annotations.
- <Fw only="react">**Hooks**</Fw><Fw only="vue">**Composables**</Fw><Fw only="svelte">**Functions**</Fw><Fw only="angular">**Services**</Fw> connect your own UI: <Fw only={['react', 'vue', 'svelte']}>`useSearch()`
  to search, `useSearchState()` to show the results, `useSearchEvent()` to react when something
  happens.</Fw><Fw only="angular">`inject(EpdfSearch)` gives you the plugin's methods to search,
  its values as signals to show the results (`search.hitCount()`), and its events as streams to
  react when something happens (`search.completed$`).</Fw>

[How it works](concepts/how-it-works) explains each in a few minutes.

## Every plugin

Each plugin is one import line. Registering a plugin and importing its components come from the
same place:

| Plugin | What it gives you | Import from |
| :-- | :-- | :-- |
| [Stage](viewing/stage) | Scrolling, zoom, layouts, page navigation | `@embedpdf/react/stage` |
| [Render](viewing/render) | Sharp pages at every zoom, deep-zoom tiles | `@embedpdf/react/render` |
| [Links](viewing/links) | Clickable links to pages and websites | `@embedpdf/react/link` |
| [Selection](text/selection) | Selecting and copying text | `@embedpdf/react/selection` |
| [Search](text/search) | Finding text, and showing every match | `@embedpdf/react/search` |
| [Annotations](annotations) | Highlights, notes, shapes, free text, and your own tools | `@embedpdf/react/annotation` |
| [Stamps](annotations/stamps) | Libraries of stamps, placed by click or by code | `@embedpdf/react/stamp` |
| [Measurements](annotations/measurements) | Distances, perimeters and areas at a real scale | `@embedpdf/react/measurement` |
| [Redaction](annotations/redaction) | Removing content for good | `@embedpdf/react/redaction` |
| [Forms](forms) | Filling and building PDF forms | `@embedpdf/react/form` |
| [Signatures](forms/signatures) | Signing documents and checking signatures | `@embedpdf/react/signature` |
| [Page edit](documents/pages) | Rotating, reordering, inserting and deleting pages | `@embedpdf/react/page-edit` |
| [Metadata](documents/metadata) | A document's title, author and your own fields | `@embedpdf/react/metadata` |
| [Actions](documents/actions) | What a PDF does by itself: buttons, scripts, printing | `@embedpdf/react/actions` |
| [View manager](documents/tabs) | Several documents side by side | `@embedpdf/react/view-manager` |
| [Interaction](ui/tools) | The active tool, cursors and touch | `@embedpdf/react/interaction` |
| [Commands](ui/commands) | Named actions with labels, icons and shortcuts | `@embedpdf/react/commands` |
| [Shell](ui/panels) | Which panels, dialogs and menus are open | `@embedpdf/react/shell` |
| [Translations](ui/translations) | Your UI in the user's language | `@embedpdf/react/i18n` |

## Local or cloud

The same viewer runs on either engine. Only how you create the engine and open a document differs;
every plugin, <Word of="hook" /> and component stays the same.

<Engine local>

The local engine runs in the browser: no server, no upload, and it works offline. It's the right
choice when the user already has the file, or when the file may not leave their device.

<CloudPdfCallout
  title="Show a 500-page PDF before it downloads"
  href="https://www.cloudpdf.com/docs/headless"
  cta="Build your viewer on CloudPDF"
>

With the CloudPDF engine, the file stays on your server and pages arrive as images, so the first
page shows at once, without downloading the whole PDF or the engine. Every call is checked on the
server, several people can review one document together, and every change is stored as it's made.
Your viewer stays the same: swap `localEngine()` for `cloudEngine()`.

</CloudPdfCallout>

</Engine>

<Engine cloud>

The CloudPDF engine keeps the file on your server. Pages arrive as images, so the first page
shows at once; every call is checked against the user's token; and annotations and form values are
stored as people make them, with everyone who has the document open seeing each other's changes.

</Engine>

## Next steps

<Cards>
  <Card
    title="Quick start"
    description="A viewer with text selection, a zoom control and a highlighter, step by step."
    href="quick-start"
  />
  <Card
    title="Setup"
    description="Install for your framework, create the engine, and render on the server."
    href="setup"
  />
  <Card
    title="How it works"
    description="Plugins, how your UI talks to them, layers, and which document a component talks to."
    href="concepts/how-it-works"
  />
</Cards>

---

### Website Src Content Docs Viewer Index (`website/src/content/docs/viewer/index.mdx`)

---
title: Viewer
description: A complete PDF viewer you add with one tag, then make your own with a few lines of config, in plain HTML, React, Vue, Svelte or Angular.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

import { Cards, Card } from '@/components/docs/cards';

{/* Served at `…/viewer/<framework>` with NO trailing slash: a relative href
    resolves against `…/viewer/`, so pages are `quick-start`,
    `customize/layout`, `concepts/how-it-works`. */}

# A complete PDF viewer, ready to embed

The viewer is everything people expect from a PDF app, in one tag: a toolbar, thumbnails, search,
annotations, comments, forms, signatures and stamps. It fits a phone and a wide screen, speaks the
user's language, and works with a keyboard and a screen reader.

<Snippet name="viewer/start/first" />

That's the whole integration. The rest of these docs are about making it yours.

## Make it yours

Everything you change is a few lines of config, the same in every framework. Start at the top and
stop when it's enough:

| You want to | Change | Page |
| :-- | :-- | :-- |
| Match your brand | Your accent color, fonts, light or dark | [Theming](customize/theming) |
| Leave features out, or make it read-only | `disable: ['forms', 'redaction']`, `readOnly: true` | [Turn features off](features/turning-off) |
| Move or remove buttons | One line per change | [Layout](customize/layout) |
| Put a toolbar at the bottom, on the side, or several | Where each bar goes | [Layout](customize/layout) |
| Add your own button | A command: a label, an icon, what it does | [Buttons & commands](customize/commands) |
| Add your own menu or panel | A list of items, or your component | [Menus](customize/menus), [Panels](customize/panels) |
| Put your own component in the toolbar | Your React, Vue, Svelte or Angular component | [Your own components](customize/components) |
| Control the viewer from your app | The `viewer` object | [The viewer in code](code/viewer) |

You never write layout code for phones, menus that open the right way, or keyboard focus. You say
what goes where, and the viewer takes care of how it behaves. A menu in a bottom toolbar opens
upward; the thumbnails slide up from the bottom on a phone; a button that doesn't fit moves into
the "More" menu.

## The viewer or headless?

EmbedPDF comes in two forms, built on the same engine and the same plugins:

- **The viewer** is finished. You configure it, theme it, and add your own buttons, menus, panels
  and components.
- **[Headless](/docs/headless)** is the parts. You build the whole interface yourself, and
  nothing appears on screen unless you put it there.

Start with the viewer. Every feature is a [headless plugin](code/plugins) with the same settings,
and inside the viewer your own components can use the headless hooks. If you outgrow the viewer,
what you learned still applies.

<Engine local>

## Local or cloud

The viewer runs on the local engine by default: the PDF opens in the browser, with no server and no
upload, and it works offline.

<CloudPdfCallout
  title="Open a 500-page PDF at once"
  href="https://www.cloudpdf.com/docs/viewer"
  cta="Use the viewer with CloudPDF"
>

With CloudPDF, the file stays on your server and pages arrive as images, so the first page shows
at once, without downloading the whole PDF or the engine. Every action is checked on the server,
and annotations and comments are shared live between everyone who has the document open. The
viewer and your config stay the same.

</CloudPdfCallout>

</Engine>

<Engine cloud>

## The CloudPDF viewer

The file stays on your server. Pages arrive as images, so the first page shows at once; every
action is checked against the user's token; and annotations, comments and form values are stored
as people make them, with everyone who has the document open seeing each other's changes.

</Engine>

## Next steps

<Cards>
  <Card
    title="Quick start"
    description="Install, show a PDF, add your color and your own button."
    href="quick-start"
  />
  <Card
    title="Layout"
    description="Toolbars at any edge, several of them, and every button where you want it."
    href="customize/layout"
  />
  <Card
    title="Your own components"
    description="Your React, Vue, Svelte or Angular components inside the viewer."
    href="customize/components"
  />
</Cards>

---

### Cloudpdf Website Src Content Docs Api Reference Index (`cloudpdf/website/src/content/docs/api-reference/index.mdx`)

---
title: CloudPDF API reference
description: Complete reference for the CloudPDF server API, with examples for all seven official SDKs.
---

import { Callout } from '@/components/docs/callout';

# CloudPDF API reference

The CloudPDF server API is what your **backend** calls: it gets documents in and
mints the short-lived tokens your viewer opens with. The browser never talks to
this API directly.

Every operation below is generated from the same OpenAPI contract that generates
the official SDKs, so the reference cannot drift from the wire.

<Callout type="info">
  The current API and SDK line is a preview. Endpoint pages display the exact canonical version,
  and your SDK language is remembered across the whole reference — pick it once below.
</Callout>

## Install

Seven official SDKs, all generated from the contract below. Your choice here is
remembered for every example in the reference.

<ApiInstall />

## Get access

<DeploymentTabs>
  <DeploymentTab mode="saas">
    On managed CloudPDF **your account is a tenant** — tenants and deployment
    configuration are operated by the platform, so the sections marked
    *Self-hosted* below don't apply to you.

    Your API key from the [dashboard](https://app.cloudpdf.com) is a **tenant token**: it can manage
    documents and mint document tokens under your account, and nothing else.
    Use it as the `token` in every example in this reference.
  </DeploymentTab>
  <DeploymentTab mode="self-hosted">
    On a self-hosted deployment **you are the operator**. Configure the root
    credential on the server:

    ```sh
    CLOUDPDF_API_AUTH_TOKENS=<a-long-random-secret>
    ```

    The API token is valid on every surface — including creating the
    [tenants](/docs/api-reference/tenants/create) that namespace your
    customers' documents. Use it as the `token` in every example, or mint
    scoped [tenant tokens](/docs/api-reference/authentication#tenant-tokens)
    for your services.
  </DeploymentTab>
</DeploymentTabs>

## Getting a document on screen

Three steps, and only the last one runs in a browser.

1. **[Initialize an upload](/docs/api-reference/documents/init)** — request upload
   access, transfer the file using the returned instructions, then
   [commit the upload](/docs/api-reference/documents/commit).
2. **[Issue a document token](/docs/api-reference/tokens/issue)** — a short-lived JWT
   pinned to one document, carrying exactly the capabilities that user should have.
3. **Open it** — hand that token to the viewer or headless components. See
   [Engine: getting started](/docs/engine).

Steps 1–2 are the API surface documented here. Step 3 is the client SDK.

## Authentication

Three credentials, and **authority mints only downward**. Each rung is weaker
than the one above it, and only the last one ever reaches a browser.

<ApiCredentials />

[How authentication works →](/docs/api-reference/authentication) — minting,
signing modes, revocation, and the full capability tables.

## Resources

<ApiResources />

---

### Cloudpdf Website Src Content Docs Engine Index (`cloudpdf/website/src/content/docs/engine/index.mdx`)

---
title: Engine
description: Open, read, change and save PDF files from JavaScript, with one typed API.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

import { Cards, Card } from '@/components/docs/cards';

# The PDF engine

<Engine local>

The EmbedPDF engine opens PDF files in the browser and lets you render their pages, read and
search their text, add annotations, fill forms, edit pages and save the result. It runs PDFium,
the PDF library inside Chrome, in a Web Worker, so your page stays responsive. Nothing is
uploaded: the file never leaves the device.

The ready-made viewer and the headless components are built on this engine. Use it directly
when you want PDF features without a viewer: a thumbnail job, a form filler, an export script.

</Engine>

<Engine cloud>

The CloudPDF engine gives your app a full PDF engine over HTTPS. Your files stay on your
CloudPDF server. The engine renders their pages, reads and searches their text, stores
annotations and form values, and checks every call against the permissions in the user's
token.

The CloudPDF viewer and the headless components are built on this engine. Use it directly when
you want PDF features without a viewer: a thumbnail job, a form filler, an export script.

</Engine>

<Engine local>

```ts
import { localEngine } from '@cloudpdf/engine';

const engine = localEngine();
const doc = await engine.open({ kind: 'bytes', bytes });

const { pages } = await doc.pages.list();
const image = await doc.page(pages[0].ref).render.image();
```

</Engine>

<Engine cloud>

```ts
import { cloudEngine } from '@cloudpdf/engine';

const engine = cloudEngine();
const doc = await engine.open({ kind: 'token', token: () => fetchDocumentToken(documentId) });

const { pages } = await doc.pages.list();
const image = await doc.page(pages[0].ref).render.image();
```

</Engine>

## What you can do

- **[Render pages](/docs/engine/documents/rendering)** as images, at any size.
- **[Read](/docs/engine/text/extraction), [search](/docs/engine/text/search) and
  [select](/docs/engine/text/selection-and-geometry)** the text on a page, and find out where
  each character is.
- **[Add and edit annotations](/docs/engine/annotations)**: highlights, notes, shapes, stamps,
  measurements. Copy them to another document with
  [import & export](/docs/engine/annotations/import-export).
- **[Fill forms](/docs/engine/forms)**, create form fields and
  [sign](/docs/engine/forms/signatures) documents.
- **[Change pages](/docs/engine/documents/pages)**: rotate, reorder, delete, insert, merge and
  split.
- **[Redact](/docs/engine/documents/redaction)** content for good, and
  **[save](/docs/engine/documents/saving)** the result.
- **[Group edits into changes](/docs/engine/documents/changes)** that apply all or none, and undo
  or redo any of them.

Every call is async, can be cancelled, and fails with a typed error you can branch on. The same
code runs on the local engine and on the CloudPDF engine: only how you create the engine and
open a file differs.

A few things exist only on the local engine: its fonts, `warmup()`, raw pixels, and saving a
document's layer or to a file. A document opened with `localEngine()` has them. Code that takes
any engine checks first:

```ts
import { isLocalDocument } from '@cloudpdf/engine';

if (isLocalDocument(doc)) {
  const layer = await doc.downloadLayer();
}
```

`isLocalEngine(engine)` does the same for an engine.

<Engine local>

## Local or cloud

This engine runs in the browser: no server, no upload, and it works offline. That makes it the
right choice when the user already has the file, or when the file may not leave their device.

<CloudPdfCallout
  title="Keep the file on your server"
  href="https://www.cloudpdf.com/docs/engine"
  cta="Read about the CloudPDF engine"
>

When the file lives on your server, the CloudPDF engine runs the same API there and checks every
call. A user sees the first page as an image, without downloading the whole PDF or the engine
(about 3 MB compressed), and a user who may not download the file never receives it. Your code
stays the same: swap `localEngine()` for `cloudEngine()`.

</CloudPdfCallout>

</Engine>

## Next steps

<Cards>
  <Card
    title="Quick start"
    description="Open a PDF, render a page, search it and add a highlight."
    href="/docs/engine/quick-start"
  />
  <Card
    title="Setup"
    description="Install the engine and create it once for your app."
    href="/docs/engine/setup"
  />
  <Card
    title="Annotations"
    description="Read, add and change annotations of every kind."
    href="/docs/engine/annotations"
  />
</Cards>

---

### Cloudpdf Website Src Content Docs Headless Index (`cloudpdf/website/src/content/docs/headless/index.mdx`)

---
title: Headless
description: Build your own PDF viewer from plugins, components and hooks, in React, Vue, Svelte or Angular.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

import { Cards, Card } from '@/components/docs/cards';

{/* Served at `…/headless/<framework>` with NO trailing slash: a relative href
    resolves against `…/headless/`, so pages are `quick-start`,
    `viewing/stage`, `concepts/how-it-works`. */}

# Build your own PDF viewer

The headless packages give you everything a PDF viewer does, without a look of their own: pages
that scroll and zoom, crisp rendering, text selection and search, annotations, forms and
signatures. You build the interface in <Framework />, and nothing appears on screen unless you put
it there.

<Engine local>

<Snippet name="start/overview" />

</Engine>

<Engine cloud>

<Snippet name="start/overview-cloud" />

</Engine>

That's a complete viewer: the pages scroll, zoom with a pinch or Ctrl and the wheel, and stay sharp
at every zoom. Everything else is a plugin you add, and a few lines of your own UI.

## Headless or the ready-made Viewer?

EmbedPDF comes in two forms, built on the same engine and plugins:

- **The [Viewer](/docs/viewer)** is finished: a toolbar, sidebars, annotation tools and search,
  ready in minutes. You configure it and theme it.
- **Headless** is the parts: you decide what the toolbar looks like, where the search box goes, and
  what happens when someone clicks an annotation.

Choose headless when the viewer is part of your product's own interface. You can start with the
Viewer and move to headless later: the plugins and their options are the same.

## How it's put together

A headless viewer has five parts. You meet them on every page:

- **The engine** opens and reads PDF files. It runs in the browser, or on your server with
  CloudPDF.
- **`<Viewer>`** starts the engine, opens documents, and holds the plugins.
- **Plugins** add one feature each: search, annotations, forms. Register the ones you need; the
  rest never reach your bundle.
- **Layers** draw inside each page: the page itself, the text selection, the annotations.
- <Fw only="react">**Hooks**</Fw><Fw only="vue">**Composables**</Fw><Fw only="svelte">**Functions**</Fw><Fw only="angular">**Services**</Fw> connect your own UI: <Fw only={['react', 'vue', 'svelte']}>`useSearch()`
  to search, `useSearchState()` to show the results, `useSearchEvent()` to react when something
  happens.</Fw><Fw only="angular">`inject(EpdfSearch)` gives you the plugin's methods to search,
  its values as signals to show the results (`search.hitCount()`), and its events as streams to
  react when something happens (`search.completed$`).</Fw>

[How it works](concepts/how-it-works) explains each in a few minutes.

## Every plugin

Each plugin is one import line. Registering a plugin and importing its components come from the
same place:

| Plugin | What it gives you | Import from |
| :-- | :-- | :-- |
| [Stage](viewing/stage) | Scrolling, zoom, layouts, page navigation | `@embedpdf/react/stage` |
| [Render](viewing/render) | Sharp pages at every zoom, deep-zoom tiles | `@embedpdf/react/render` |
| [Links](viewing/links) | Clickable links to pages and websites | `@embedpdf/react/link` |
| [Selection](text/selection) | Selecting and copying text | `@embedpdf/react/selection` |
| [Search](text/search) | Finding text, and showing every match | `@embedpdf/react/search` |
| [Annotations](annotations) | Highlights, notes, shapes, free text, and your own tools | `@embedpdf/react/annotation` |
| [Stamps](annotations/stamps) | Libraries of stamps, placed by click or by code | `@embedpdf/react/stamp` |
| [Measurements](annotations/measurements) | Distances, perimeters and areas at a real scale | `@embedpdf/react/measurement` |
| [Redaction](annotations/redaction) | Removing content for good | `@embedpdf/react/redaction` |
| [Forms](forms) | Filling and building PDF forms | `@embedpdf/react/form` |
| [Signatures](forms/signatures) | Signing documents and checking signatures | `@embedpdf/react/signature` |
| [Page edit](documents/pages) | Rotating, reordering, inserting and deleting pages | `@embedpdf/react/page-edit` |
| [Metadata](documents/metadata) | A document's title, author and your own fields | `@embedpdf/react/metadata` |
| [Actions](documents/actions) | What a PDF does by itself: buttons, scripts, printing | `@embedpdf/react/actions` |
| [View manager](documents/tabs) | Several documents side by side | `@embedpdf/react/view-manager` |
| [Interaction](ui/tools) | The active tool, cursors and touch | `@embedpdf/react/interaction` |
| [Commands](ui/commands) | Named actions with labels, icons and shortcuts | `@embedpdf/react/commands` |
| [Shell](ui/panels) | Which panels, dialogs and menus are open | `@embedpdf/react/shell` |
| [Translations](ui/translations) | Your UI in the user's language | `@embedpdf/react/i18n` |

## Local or cloud

The same viewer runs on either engine. Only how you create the engine and open a document differs;
every plugin, <Word of="hook" /> and component stays the same.

<Engine local>

The local engine runs in the browser: no server, no upload, and it works offline. It's the right
choice when the user already has the file, or when the file may not leave their device.

<CloudPdfCallout
  title="Show a 500-page PDF before it downloads"
  href="https://www.cloudpdf.com/docs/headless"
  cta="Build your viewer on CloudPDF"
>

With the CloudPDF engine, the file stays on your server and pages arrive as images, so the first
page shows at once, without downloading the whole PDF or the engine. Every call is checked on the
server, several people can review one document together, and every change is stored as it's made.
Your viewer stays the same: swap `localEngine()` for `cloudEngine()`.

</CloudPdfCallout>

</Engine>

<Engine cloud>

The CloudPDF engine keeps the file on your server. Pages arrive as images, so the first page
shows at once; every call is checked against the user's token; and annotations and form values are
stored as people make them, with everyone who has the document open seeing each other's changes.

</Engine>

## Next steps

<Cards>
  <Card
    title="Quick start"
    description="A viewer with text selection, a zoom control and a highlighter, step by step."
    href="quick-start"
  />
  <Card
    title="Setup"
    description="Install for your framework, create the engine, and render on the server."
    href="setup"
  />
  <Card
    title="How it works"
    description="Plugins, how your UI talks to them, layers, and which document a component talks to."
    href="concepts/how-it-works"
  />
</Cards>

---

### Cloudpdf Website Src Content Docs React Index (`cloudpdf/website/src/content/docs/react/index.mdx`)

---
title: React
description: Use CloudPDF in your React application.
---

# React

Integrate CloudPDF into your React application. This is a placeholder page;
section content will be added here.

## Getting started

This section will cover installation, the viewer component, and headless usage
in React.

---

### Cloudpdf Website Src Content Docs Svelte Index (`cloudpdf/website/src/content/docs/svelte/index.mdx`)

---
title: Svelte
description: Use CloudPDF in your Svelte application.
---

# Svelte

Integrate CloudPDF into your Svelte application. This is a placeholder page;
section content will be added here.

## Getting started

This section will cover installation, the viewer component, and headless usage
in Svelte.

---

### Cloudpdf Website Src Content Docs Viewer Index (`cloudpdf/website/src/content/docs/viewer/index.mdx`)

---
title: Viewer
description: A complete PDF viewer you add with one tag, then make your own with a few lines of config, in plain HTML, React, Vue, Svelte or Angular.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

import { Cards, Card } from '@/components/docs/cards';

{/* Served at `…/viewer/<framework>` with NO trailing slash: a relative href
    resolves against `…/viewer/`, so pages are `quick-start`,
    `customize/layout`, `concepts/how-it-works`. */}

# A complete PDF viewer, ready to embed

The viewer is everything people expect from a PDF app, in one tag: a toolbar, thumbnails, search,
annotations, comments, forms, signatures and stamps. It fits a phone and a wide screen, speaks the
user's language, and works with a keyboard and a screen reader.

<Snippet name="viewer/start/first" />

That's the whole integration. The rest of these docs are about making it yours.

## Make it yours

Everything you change is a few lines of config, the same in every framework. Start at the top and
stop when it's enough:

| You want to | Change | Page |
| :-- | :-- | :-- |
| Match your brand | Your accent color, fonts, light or dark | [Theming](customize/theming) |
| Leave features out, or make it read-only | `disable: ['forms', 'redaction']`, `readOnly: true` | [Turn features off](features/turning-off) |
| Move or remove buttons | One line per change | [Layout](customize/layout) |
| Put a toolbar at the bottom, on the side, or several | Where each bar goes | [Layout](customize/layout) |
| Add your own button | A command: a label, an icon, what it does | [Buttons & commands](customize/commands) |
| Add your own menu or panel | A list of items, or your component | [Menus](customize/menus), [Panels](customize/panels) |
| Put your own component in the toolbar | Your React, Vue, Svelte or Angular component | [Your own components](customize/components) |
| Control the viewer from your app | The `viewer` object | [The viewer in code](code/viewer) |

You never write layout code for phones, menus that open the right way, or keyboard focus. You say
what goes where, and the viewer takes care of how it behaves. A menu in a bottom toolbar opens
upward; the thumbnails slide up from the bottom on a phone; a button that doesn't fit moves into
the "More" menu.

## The viewer or headless?

EmbedPDF comes in two forms, built on the same engine and the same plugins:

- **The viewer** is finished. You configure it, theme it, and add your own buttons, menus, panels
  and components.
- **[Headless](/docs/headless)** is the parts. You build the whole interface yourself, and
  nothing appears on screen unless you put it there.

Start with the viewer. Every feature is a [headless plugin](code/plugins) with the same settings,
and inside the viewer your own components can use the headless hooks. If you outgrow the viewer,
what you learned still applies.

<Engine local>

## Local or cloud

The viewer runs on the local engine by default: the PDF opens in the browser, with no server and no
upload, and it works offline.

<CloudPdfCallout
  title="Open a 500-page PDF at once"
  href="https://www.cloudpdf.com/docs/viewer"
  cta="Use the viewer with CloudPDF"
>

With CloudPDF, the file stays on your server and pages arrive as images, so the first page shows
at once, without downloading the whole PDF or the engine. Every action is checked on the server,
and annotations and comments are shared live between everyone who has the document open. The
viewer and your config stay the same.

</CloudPdfCallout>

</Engine>

<Engine cloud>

## The CloudPDF viewer

The file stays on your server. Pages arrive as images, so the first page shows at once; every
action is checked against the user's token; and annotations, comments and form values are stored
as people make them, with everyone who has the document open seeing each other's changes.

</Engine>

## Next steps

<Cards>
  <Card
    title="Quick start"
    description="Install, show a PDF, add your color and your own button."
    href="quick-start"
  />
  <Card
    title="Layout"
    description="Toolbars at any edge, several of them, and every button where you want it."
    href="customize/layout"
  />
  <Card
    title="Your own components"
    description="Your React, Vue, Svelte or Angular components inside the viewer."
    href="customize/components"
  />
</Cards>

---

### Cloudpdf Website Src Content Docs Vue Index (`cloudpdf/website/src/content/docs/vue/index.mdx`)

---
title: Vue
description: Use CloudPDF in your Vue application.
---

# Vue

Integrate CloudPDF into your Vue application. This is a placeholder page;
section content will be added here.

## Getting started

This section will cover installation, the viewer component, and headless usage
in Vue.

---

### Website Src Content Docs Engine Annotations Index (`website/src/content/docs/engine/annotations/index.mdx`)

---
title: Annotations
description: List, add, change, reorder and delete annotations, with the fields and styles every kind shares.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

import { Callout } from '@/components/docs/callout';
import { CardGrid, GridCard } from '@/components/docs/cards';

# Annotations

Annotations are what people add on top of a page: highlights, sticky notes, shapes, stamps,
free text, links. They're stored in the PDF, so every PDF app shows them, and they stay separate
from the page's own content, so you can change or remove them later.

```ts
const page = doc.page(pageRef);

const { annotation } = await page.annotations.create({
  subtype: 'square',
  box: { x: 72, y: 92, width: 200, height: 100 },
  color: '#0078ff',
});

await page.annotations.update(annotation.ref, { contents: 'Check this table' });
await page.annotations.delete(annotation.ref);
```

## Kinds

Each kind has its own page with an example and its fields:

<CardGrid columns={2}>
  <GridCard
    icon="annotation"
    title="Highlights & text markup"
    description="Highlight, underline, squiggly and strikeout over text."
    href="/docs/engine/annotations/text-markup"
  />
  <GridCard
    icon="note"
    title="Notes & replies"
    description="Sticky notes, their popups, and threads of replies."
    href="/docs/engine/annotations/notes-and-replies"
  />
  <GridCard
    icon="free-text"
    title="Free text"
    description="Text boxes and callouts, with fonts and rich text."
    href="/docs/engine/annotations/free-text"
  />
  <GridCard
    icon="shape"
    title="Shapes & drawing"
    description="Rectangles, ellipses, lines, polygons and freehand ink."
    href="/docs/engine/annotations/shapes"
  />
  <GridCard
    icon="stamp"
    title="Stamps"
    description="Images and drawings placed on a page."
    href="/docs/engine/annotations/stamps"
  />
  <GridCard
    icon="link"
    title="Links"
    description="Areas that go to another page or open a website."
    href="/docs/engine/annotations/links"
  />
  <GridCard
    icon="attachment"
    title="File attachments"
    description="A file pinned to a spot on the page."
    href="/docs/engine/annotations/file-attachments"
  />
  <GridCard
    icon="measurement"
    title="Measurements"
    description="Distances, perimeters and areas at a real-world scale."
    href="/docs/engine/annotations/measurements"
  />
</CardGrid>

## List annotations

Every annotation in the document, or the ones on one page:

```ts
const { annotations } = await doc.annotations.list(); // every page
const { annotations } = await doc.annotations.list({ pages: [pageA, pageB] }); // some pages
const { annotations } = await doc.page(pageRef).annotations.list(); // one page
```

A list is cheap even for a whole document: the engine reads the annotations without loading the
pages. Each annotation names its `page`, and the list's `pages` has one entry per page it read.

Each annotation has a `subtype` (its kind), a `rect` (where it is on its page) and a `ref` (its
address). Keep the `ref`: it's how you change or delete the annotation later.

Positions are in points from the top-left corner of the page, with y going down, like every
position in the engine: a box is `{ x, y, width, height }` and a point `{ x, y }`. See
[Coordinates](/docs/engine/concepts/coordinates).

```ts
for (const annotation of annotations) {
  console.log(annotation.subtype, annotation.rect, annotation.contents);
}
```

A list includes kinds the engine can't create yet: they come back as `subtype: 'unsupported'`,
with their position and common fields. They stay in the PDF untouched.

Form widgets are annotations too, of kind `'widget'`, but they're the form's: a list here never
holds one, and `doc.forms.list()` returns them with the fields. You place and change them through
the form; see [Creating form fields](/docs/engine/forms/authoring).

## Fields every annotation has

Besides `subtype`, `rect` and `ref`, every kind has these fields:

| Field      | What it is                                                                                               |
| :--------- | :------------------------------------------------------------------------------------------------------- |
| `contents` | A comment, the text PDF apps show for the annotation.                                                    |
| `subject`  | A short title PDF apps show with the comment, such as `'Question'`.                                      |
| `nm`       | A unique name on its page. See [Refs & identity](/docs/engine/concepts/identity).                        |
| `reply`    | The annotation it answers, or `null`. See [Notes & replies](/docs/engine/annotations/notes-and-replies). |
| `popup`    | The ref of its popup window, or `null`. See [Popups](/docs/engine/annotations/notes-and-replies#popups). |
| `groupId`  | The group it belongs to, when you work with groups.                                                      |

Each annotation also has the [flags](#flags) and the fields that say [who wrote it](#who-wrote-it).

## Add an annotation

`create()` takes the kind and its fields, and returns the new annotation:

```ts
const { annotation } = await page.annotations.create({
  subtype: 'circle',
  box: { x: 60, y: 392, width: 120, height: 100 },
  interiorColor: '#1e90ff',
});
```

Fields you leave out get the kind's default. The engine draws the annotation's appearance for
you, so it shows the same in every PDF app.

Stamps and file attachments also carry bytes (an image, a file). Pass those in a second
argument; see [Stamps](/docs/engine/annotations/stamps) and
[File attachments](/docs/engine/annotations/file-attachments).

## Change an annotation

`update()` takes the `ref` and only the fields you want to change:

```ts
await page.annotations.update(annotation.ref, {
  color: '#dc143c',
  contents: 'Reviewed',
});
```

A field you leave out keeps its value. A field set to `null` is removed, for example
`{ interiorColor: null }` for no fill. A field's value is replaced whole (a `file`, a list of
points), except rich text's `body` style, which merges.

You can also pass an annotation you read straight back: every field it has is one `update()`
accepts. That makes "change a copy, then save it" code simple:

```ts
await page.annotations.update(annotation.ref, { ...annotation, opacity: 0.5 });
```

Fields the engine writes (`author`, the dates, `userId`, `ref`…) are accepted and ignored. For
free text, send `contents` or `richText`, not both with different text.

### Move or resize by `rect`

An update takes a new `rect` for every annotation, as a script's `annot.rect` does in Acrobat. A
note, a file, a link, a popup and a redaction take it as it is.

Every other annotation is drawn from its shape: rectangles, ellipses, text boxes, stamps, carets
and form fields' widgets from their `box`, lines, polylines, polygons and ink from their points,
and text markup from its quads. For these, a new `rect` puts the shape there: each point is mapped from the old
`rect` onto the new one, each axis on its own. A `rect` of the same size moves the drawing, and a
bigger one stretches it:

```ts
const { rect } = line;

// 40 points right, 20 down
await page.annotations.update(line.ref, {
  rect: { ...rect, x: rect.x + 40, y: rect.y + 20 },
});
```

The engine then works out `rect` again from the shape. After a resize it can differ a little from
the one you sent: a line keeps its width, and its endings keep their size. The `rect` you read,
sent back, changes nothing, so a read passed straight back works.

- A new `rect` with a new shape (`box`, points or quads) fails with `InvalidArg`: send one or the
  other.
- A turned drawing can only move. A `rect` of another size fails with `InvalidArg`; resize its
  `box` or its points instead. A box at a quarter turn is the exception: it stands upright on the
  page, so a `rect` resizes it along its own sides.
- A `rect` that would squash a drawing to no width or no height fails with `InvalidArg`.

`shapeForRect(annotation, rect)` gives the fields that put an annotation at a `rect`, by the same
rule. Use it to place a copy somewhere else:

```ts
import { shapeForRect } from '@embedpdf/engine';

await page.annotations.create({ ...copy, ...shapeForRect(copy, target) });
```

A create works out a drawn annotation's `rect` from its shape, whatever `rect` you send, so a
copy of another app's drawing keeps its shape exactly. A box turned a quarter can be created by
its `rect` instead of its `box`; see [Turned shapes](/docs/engine/annotations/shapes#turned-shapes).

## Delete and reorder

```ts
await page.annotations.delete(annotation.ref);

await page.annotations.reorder([annotation.ref], 'start'); // to the back
await page.annotations.reorder([annotation.ref], { after: other.ref }); // just above another
```

Deleting an annotation also deletes its popup, if it has one, and its replies; see
[Delete a note](/docs/engine/annotations/notes-and-replies#delete-a-note).

Annotations on a page are drawn in order, so the last one is on top. `reorder(refs, position)`
moves annotations together, in the order you pass them, right after or before another annotation
of the page, or to the back (`'start'`) or the front (`'end'`). It resolves the page's new order,
`order`, as refs, and fires `annotations.reordered`.

Widgets are drawn above every annotation, in an order of their own, so a widget is never a
neighbour here: `reorder()` refuses one with `InvalidArg`. Restack widgets with
[`doc.forms.reorderWidgets()`](/docs/engine/forms/authoring#move-and-style-widgets).

## Style

Most kinds share the same style fields:

| Field           | What it is                                              | Default   |
| :-------------- | :------------------------------------------------------ | :-------- |
| `color`         | Line color, such as `'#1e90ff'`                         | per kind  |
| `interiorColor` | Fill color of closed shapes, or `null` for no fill      | no fill   |
| `opacity`       | From `0` (invisible) to `1` (solid)                     | `1`       |
| `strokeWidth`   | Line width in points                                    | `1`       |
| `borderStyle`   | `'solid'` or `'dashed'`                                 | `'solid'` |
| `dashArray`     | The pattern a `'dashed'` border draws, such as `[3, 2]` | `[3]`     |

A pattern alone doesn't make a border dashed: a `'solid'` border keeps its `dashArray` and draws
solid. `dashArray: null` on a dashed border draws the default `[3]`, 3 points on and 3 off. A
border from another app can read `'beveled'` or `'inset'`; only form widgets draw those, so a
write takes them only sent back unchanged.

Colors are `'#rrggbb'`, as in CSS: the engine gives them in lowercase and takes either case. They
never include transparency: that's what `opacity` is for. The default color depends on the kind
(yellow for a highlight, red for a shape, black for free text); each kind's page lists its
defaults.

A PDF can store a color as gray or CMYK. It reads as the same color in `'#rrggbb'`, and sent back
unchanged it stays as the file has it, so passing a read straight back rewrites nothing.

## Who wrote it

The engine records who created and last changed each annotation, from the user the document was
opened for. You read these fields; you don't set them:

| Field        | What it is                                          |
| :----------- | :-------------------------------------------------- |
| `author`     | The display name shown in PDF apps.                 |
| `createdAt`  | When it was created, as an ISO date.                |
| `modifiedAt` | When it was last changed.                           |
| `userId`     | The id of the user who owns it.                     |
| `createdBy`  | The id of the user who created it.                  |
| `modifiedBy` | The id of the user who last changed it.             |
| `groupId`    | The group it belongs to, when you work with groups. |

<Engine local>

The user comes from the `identity` you pass when you open the document; see
[Opening documents](/docs/engine/documents/opening#who-is-working-on-it).

</Engine>

<Engine cloud>

The user comes from the identity in their token; see
[Authentication](/docs/engine/authentication#what-a-token-carries).

</Engine>

## Flags

Every annotation has the standard PDF flags as `true`/`false` fields. Set the ones you need; the
rest stay off (on create) or keep their value (on update). The exception is `print`: a new
annotation prints unless you set `print: false`, as in Acrobat. A popup doesn't print.

```ts
await page.annotations.update(annotation.ref, { print: true, locked: true });
```

| Flag             | What it does                                            |
| :--------------- | :------------------------------------------------------ |
| `print`          | Printed with the page. Off: shown on screen only.       |
| `hidden`         | Not shown and not printed.                              |
| `noView`         | Not shown on screen (can still print).                  |
| `locked`         | Can't be moved, resized or deleted in PDF apps.         |
| `lockedContents` | Its text can't be changed in PDF apps.                  |
| `readOnly`       | Can't be changed at all in PDF apps.                    |
| `noZoom`         | Keeps its size when the page is zoomed (notes, icons).  |
| `noRotate`       | Stays upright when the page is rotated.                 |
| `invisible`      | Hidden when the app doesn't know the annotation's kind. |
| `toggleNoView`   | Flips `noView` when the pointer is over it.             |

Flags are instructions for PDF apps. The engine itself still lets you change a locked
annotation; the viewer's annotation tools respect the flags in their interface.

## Permissions

Reading annotations needs `doc.annotate.read`. Creating, changing and deleting them needs a
permission for that action, which can be limited to the user's own annotations:
`annotations:update:self` lets a user edit what they created and nothing else. See
[Permissions](/docs/engine/concepts/permissions#annotations).

<Engine local>

<CloudPdfCallout
  title="Let many people annotate one document"
  href="https://www.cloudpdf.com/docs/engine"
  cta="Collaborate with CloudPDF"
>

With CloudPDF, annotations are stored on your server as people make them. Everyone with the
document open gets each other's changes as [events](/docs/engine/concepts/events), and the server
makes sure people only change the annotations they may change.

</CloudPdfCallout>

</Engine>

## More

- [Import & export](/docs/engine/annotations/import-export): copy annotations to another
  document, or store them apart from the PDF.
- [Drawing annotations yourself](/docs/engine/annotations/appearances): render annotations as
  separate images, bake them into the page, or export them as a PDF.
- [Refs & identity](/docs/engine/concepts/identity): what a `ref` contains, and why you never
  build one.
- [Changes & undo](/docs/engine/documents/changes): make several annotation edits as one action,
  and undo them.

---

### Website Src Content Docs Engine Forms Index (`website/src/content/docs/engine/forms/index.mdx`)

---
title: Forms
description: Read a PDF form's fields and values, fill them in, reset them, and move fields and values between documents.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

# Forms

A PDF form is a set of fields: text boxes, checkboxes, radio buttons, dropdowns and lists. The
engine reads every field with its value, fills fields in, and redraws them so the new value shows
in every PDF app.

```ts
const { fields } = await doc.forms.list();

const name = fields.find((field) => field.name === 'billing.name')!;
await doc.forms.setValue(name.ref, { value: 'Ada Lovelace' });
```

## Read the form

`list()` returns every field in the document:

```ts
const { fields } = await doc.forms.list();

for (const field of fields) {
  console.log(field.name, field.family);
}
```

Every field has these:

| Field           | What it is                                                                                                                          |
| :-------------- | :---------------------------------------------------------------------------------------------------------------------------------- |
| `name`          | The full name, with the groups it's in: `billing.name`.                                                                             |
| `family`        | `'text'`, `'checkbox'`, `'radio'`, `'combobox'`, `'listbox'`, `'pushbutton'`, `'signature'` or `'unknown'`.                         |
| `ref`           | Its address. Keep it: it's how you fill, reset or change the field.                                                                 |
| `readOnly`      | `true` when PDF apps don't let the user change the value.                                                                           |
| `required`      | `true` when PDF apps ask for a value before the form is submitted.                                                                  |
| `noExport`      | `true` when the field is left out when the form is submitted.                                                                       |
| `alternateName` | The tooltip PDF apps show, also read by screen readers, or `null`.                                                                  |
| `groupId`       | The group the field belongs to: who fills it in, such as `'buyer'`, or `null`. See [Signers and groups](/docs/engine/forms/groups). |
| `widgets`       | Where the field shows on the pages (see [Widgets](#widgets)).                                                                       |

A document without a form returns an empty `fields`. The list's `formKind` says what the document
has: `'none'`, `'acroform'` (a regular PDF form) or `'xfa'` (see [Good to know](#xfa-forms)).

The list also has `needsAppearances`, `true` when the form asks PDF apps to draw its fields
themselves ([`repair()`](/docs/engine/forms/authoring#repair-a-broken-form) can clear it), and
`calculationOrder` (see [Calculations](#calculations-formatting-and-validation)).

### Values

Each family has its own value fields. Check `family` first, and TypeScript knows which ones the
field has:

```ts
if (field.family === 'text') console.log(field.value); // string
if (field.family === 'checkbox') console.log(field.checked); // boolean
if (field.family === 'listbox') console.log(field.selectedValues); // string[]
```

| Family       | Its value                                                          | Also                                                                                             |
| :----------- | :----------------------------------------------------------------- | :----------------------------------------------------------------------------------------------- |
| `text`       | `value`, a string                                                  | `defaultValue`, `maxLength` (`null`: no limit), `multiline`, `password`, `comb`                  |
| `checkbox`   | `checked`                                                          | `exportValue`: what the form data holds while it's checked                                       |
| `radio`      | `value`: the export value of the chosen button, or `'Off'`         | `noToggleToOff`, `radiosInUnison`                                                                |
| `combobox`   | `value`                                                            | `options`, `edit` (free text allowed), `defaultValue`                                            |
| `listbox`    | `selectedValues`, in option order                                  | `options`, `multiSelect`, `defaultValue` (option values), `topIndex` (the option in its top row) |
| `pushbutton` | none: a button only runs an action                                 | Each widget's `caption`, on its row in `widgets`                                                 |
| `signature`  | none here: see [Digital signatures](/docs/engine/forms/signatures) |                                                                                                  |

Each option of a dropdown (`combobox`) or list is `{ label, value, selected }`. `label` is what the
user sees; `value` is what the form stores, and what you pass when you fill the field in.

### Who created and filled it in

A field records who created it and who last filled it in, from the identity the document was opened
with:

```ts
field.createdBy; // 'user-17': who created the field, or null
field.filledBy; // 'user-42': whose fill last changed the value
field.filledByName; // 'Ann de Vries'
field.filledAt; // '2026-10-08T10:42:00Z'
```

| Field                                  | What it is                                                                                                                                                                            |
| :------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `createdBy`, `createdAt`               | Who created the field, and when. `null` for a field another app made.                                                                                                                 |
| `filledBy`, `filledByName`, `filledAt` | The user whose fill last changed the value, their name, and when. A calculated value counts as filled in by whoever's fill calculated it. `null` after a reset, or an anonymous fill. |
| `importedBy`                           | Who imported the field with its attribution kept.                                                                                                                                     |

The engine stamps these; a write can't set them. A fill that leaves the value as it was changes
none of them, and undoing a fill puts the previous ones back. A widget has no attribution of its own:
its field's is the one.

### Widgets

A field shows on a page through a **widget**: the box you see and click. Most fields have one. A
radio group is one field with a widget per button, and a field repeated on every page (a
reference number in the header, say) has a widget per page, all showing the same value.

Each widget in `field.widgets` has its `page` and its `ref`. The form also lists every widget in
`widgets`: where it is (`rect`, in points from the page's top-left), how it looks, its state,
what it does when clicked (`actions`), and the ref of the field it belongs to (`field`). So one read is enough to draw your own form layer
over a page:

```ts
const { widgets } = await doc.forms.list();

for (const widget of widgets) drawBox(widget.page, widget.rect, widget.field);
```

Checkbox and radio widgets in `field.widgets` also have their `exportValue` (what the form data
holds when this widget is checked) and `checked`.

A form field's widgets come with the form, never with the annotations: `page.annotations.list()`
doesn't return them, and reading them takes `doc.forms.read`, not `doc.annotate.read`. Each page's
widget images, every look and state, come from `page.forms.renderAppearances()`.

## Find a field

The `ref` from `list()` is the best address: it keeps working when the field is renamed. When you
know a field by its name instead (from your backend, or from data you stored), make a ref from the name:

```ts
import { toFieldRef } from '@embedpdf/engine';

const field = await doc.forms.get(toFieldRef('billing.name'));
```

The name is the full name, with dots between the groups. A field that isn't there fails with
`NotFound`.

## Fill in a field

`setValue()` takes the field's `ref` and its new value, in the same fields a read returns. What
you read, you can write back:

```ts
await doc.forms.setValue(name.ref, { value: 'Ada Lovelace' }); // text
await doc.forms.setValue(agree.ref, { checked: true }); // checkbox
await doc.forms.setValue(plan.ref, { value: 'yearly' }); // radio group
await doc.forms.setValue(country.ref, { value: 'NL' }); // dropdown
await doc.forms.setValue(toppings.ref, { selectedValues: ['cheese', 'olives'] }); // list
```

| Family     | Value                                                                           |
| :--------- | :------------------------------------------------------------------------------ |
| `text`     | `{ value }`; `null` empties it                                                  |
| `checkbox` | `{ checked }`                                                                   |
| `radio`    | `{ value }`: the export value of the button to choose; `'Off'` or `null` clears |
| `combobox` | `{ value }`: an option value; `null` clears                                     |
| `listbox`  | `{ selectedValues }`: option values; `[]` clears                                |

`setValue()` returns the field as it is now:

```ts
const { field } = await doc.forms.setValue(name.ref, { value: 'Ada' });
```

The engine redraws the field's widgets, so the page shows the new value in every PDF app. Render
the page again to see it.

### What the engine checks

The engine checks a value before it writes anything. A call that fails with `InvalidArg` leaves
the field as it was:

- The value must be the family's own: `{ checked: true }` on a text field fails.
- A dropdown or list takes option values, not labels. A dropdown with `edit: true` also takes
  free text. More than one value needs a list with `multiSelect`.
- A radio group's `value` must be the export value of one of its buttons.
- A radio group with `noToggleToOff` can't be cleared once a button is chosen.
- Push buttons and signature fields hold no value you can set.

Some things are fixed up rather than refused: text longer than `maxLength` is cut to fit, and
in a radio group with `radiosInUnison`, the buttons that share an export value check together.

`readOnly` is an instruction for PDF apps: they don't let the user change the field. The engine
still accepts your own writes, for example a total your code calculates.

## Reset

`reset()` puts back each field's `defaultValue`, or empties it when it has none. Without an
argument it resets the whole form; pass a ref, or a list of refs, to reset only those fields:

```ts
await doc.forms.reset(); // every field
await doc.forms.reset(name.ref);
await doc.forms.reset([name.ref, email.ref]);
```

It returns the fields it changed, as they are now. A field that already holds its default is
left alone and isn't in the list:

```ts
const { fields } = await doc.forms.reset();

console.log(`${fields.length} fields reset`);
```

Fields that hold no value, such as push buttons and signature fields, are skipped. Resetting the
whole form also skips the fields a signature locked and [fields stored inline](#fields-stored-inline);
name one of those yourself and the call fails, the same as a `setValue()` would.

## Move fields and values between documents

`export()` takes fields out as one bundle, each with its value. `importValues()` fills another
document's fields of the same name from it, and `import()` copies the fields themselves:

```ts
const bundle = await doc.forms.export();

const { fields, dropped } = await other.forms.importValues(bundle);
```

Each value is checked like a `setValue()`. A field the document doesn't have, a value it can't
take, or a field a signature locked is left out and listed in `dropped`; the rest still land, as
one change you can undo. See [Import & export](/docs/engine/forms/import-export).

## Events

`setValue()`, `reset()` and `importValues()` fire `forms.valueSet`, one per field that changed.
Each carries what the call returned for that field:

```ts
doc.events.on('forms.valueSet', ({ field }) => {
  updateSummary(field);
});
```

When one call changes several fields, such as `reset()` or `importValues()`, their events share
`origin.tx`, like an annotation import.

<Engine cloud>

Each value is saved on the server as you set it. Everyone with the document open gets the event,
with `origin.kind: 'remote'` for other people's changes. See
[Events](/docs/engine/concepts/events).

</Engine>

## Permissions

Reading the form needs `doc.forms.read`, and exporting it `doc.download` too. Filling in,
resetting and importing values needs `doc.forms.fill`, which includes `doc.forms.read`. See
[Permissions](/docs/engine/concepts/permissions).

A signed document can take filling away, for the whole form or for single fields. The call then
fails with `ProtectedDocument`; see
[Digital signatures](/docs/engine/forms/signatures#protection-what-signatures-allow).

## More

- [Creating form fields](/docs/engine/forms/authoring): add fields and widgets, change and delete
  them, repair a broken form.
- [Import & export](/docs/engine/forms/import-export): copy fields from one document to another,
  or fill a form in from a bundle.
- [Digital signatures](/docs/engine/forms/signatures): sign a signature field and check the
  signatures in a document.
- [Changes & undo](/docs/engine/documents/changes): fill in several fields as one action, and
  undo it.

## Good to know

### Calculations, formatting and validation

The engine doesn't run the form's JavaScript. `setValue()` stores the value you give it: it
doesn't recalculate a total, format a date or an amount, or run a validation script. PDF apps run
those scripts while a user types.

A field's scripts are in `field.actions` (`keystroke`, `format`, `validate` and `calculate`), and
the list's `calculationOrder` holds the fields whose totals are calculated, in order. To write them,
see [Scripts and calculations](/docs/engine/forms/authoring#scripts-and-calculations).

The viewer's form plugin runs these scripts and writes their results as one
[change](/docs/engine/documents/changes): `forms.setValue`, `forms.reset`, `forms.setDisplay`
(whether the field shows and prints) and `forms.setAppearanceText` (the formatted text the field
shows, while its value stays the same) ops, in the order the scripts asked for them. All of them
are written, or none is: a field a signature locked refuses the whole change. You only need these
ops when you run form scripts yourself.

### Recovered fields

Some PDFs have fields that show on the page but are missing from the form's list of fields, a
bug in the tool that made them. The engine finds them anyway: they come back with
`origin: 'recovered'` and fill in like any other field. Other PDF apps may not see them;
[`repair()`](/docs/engine/forms/authoring#repair-a-broken-form) fixes the file.

### A field on several pages

A field's widgets can be on different pages. The `meta` of a `setValue()` or `reset()` result
lists every widget whose look changed, with its page (`meta.changedWidgets`). For a radio group
that's the button switched off and the one switched on. Use it to render only the pages that
changed.

`meta.redrawnWidgets` lists those whose picture the engine drew again, such as a text field's
widgets after a new value. A checkbox or radio button that switches shows another picture it
already has, so it isn't in the list.

### Checkboxes with several widgets

A checkbox can have several widgets, each with its own `exportValue`: a "Yes / No" pair, for
example. `{ checked: true }` checks the first widget; `{ value: 'No' }` checks the widget with
that export value.

In the file, each checkbox and radio widget also has an `onState`: the name of its checked look
(PDF's appearance state). It's usually the same as the export value, and widgets the engine
creates always use their `exportValue` for it. You never need it to fill a field in.

The field's value in the file (`valueEntry`) is the checked widget's `onState`, or `'Off'`, as the
PDF standard says. Some tools write the widget's position instead (`'0'`, `'1'`); such a value
still resets to that widget.

### XFA forms

`formKind` is `'xfa'` when the PDF also carries an XFA form, an older XML form format. The
engine works with the document's regular fields and never runs XFA.

### Fields stored inline

A field that the file stores inside another object, rather than as an object of its own, can be
read but not changed: writes fail with `InvalidArg`. PDF tools rarely write fields this way.

---

### Website Src Content Docs Headless Annotations Index (`website/src/content/docs/headless/annotations/index.mdx`)

---
title: Annotations
description: Show the annotations in a PDF, let people draw new ones, and work with them from code.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

import { Cards, Card } from '@/components/docs/cards';

{/* This page is served at `…/headless/<framework>/annotations` with NO trailing
    slash, so a relative href resolves against `…/headless/<framework>/`.
    Sibling pages are `annotations/tools`, other sections `text/selection`. */}

# Annotations

Annotations are what people add on top of a page: highlights, sticky notes, shapes, stamps and
text boxes. The annotation plugin shows the ones a PDF already has, lets people draw and edit new
ones, and gives your code the same abilities. Every annotation is saved into the PDF, so it looks
the same in every other PDF app.

## Show the annotations in a PDF

Register `interactionPlugin()` and `annotationPlugin()`, then put an `<AnnotationLayer>` above the
rendered page:

<Example name="annotation/layer" />

> **Try it:** click the rectangle and drag it, or a corner to resize it. Double-click the text box
> to type in it.

<Snippet name="annotations/index/layer" />

While the annotation layer is on the page, the render layer leaves the annotations out of the page
picture by itself. The annotation layer draws them instead, so they can be selected, moved and
changed.

`interactionPlugin()` is the shared input system. Every tool, from text selection to drawing,
speaks through it; see [Selecting & copying](text/selection) for how it works.

## Let people draw

A **tool** says what a click or a drag on the page creates. Activate one and people can draw:

<Example name="annotation/draw" />

> **Try it:** drag on the page to draw a rectangle, then pick another tool. **Highlight** marks the
> text you select.

<Snippet name="annotations/index/toolbar" />

- **`pointer`** selects: click an annotation to select it, then move, resize or turn it.
- **`square`** draws a rectangle: drag, or just click for a default-size one.
- **`ink`** draws freehand.
- **`highlight`** highlights the text you select.
- **`note`** drops a sticky note where you click.

After you draw, the new annotation is selected and the tool stays active, so you can draw another.
You can change both; see [What happens after drawing](annotations/tools#what-happens-after-drawing).

## How it fits together

Everything on these pages is one of five things:

| Thing          | What it is                                                                         | Where you use it                           |
| :------------- | :--------------------------------------------------------------------------------- | :----------------------------------------- |
| **Annotation** | A record in the PDF: its kind, position, colors and text.                          | `annotation.list()`                        |
| **Tool**       | What a click or drag creates, with which defaults, cursor and preview.             | `annotationPlugin({ tools })`              |
| **Layer**      | What each page draws: the annotations, the selection box and handles, the preview. | `<AnnotationLayer>`                        |
| **Overlay**    | Your own UI attached to an annotation, such as a menu or a hover card.             | `<AnnotationMenu>`                         |
| **API**        | Reading, creating, changing and deleting annotations, and listening for changes.   | `useAnnotation()`                          |

An annotation here is the same record the engine reads and writes: the same kinds, the same fields
and the same names. What you learn in the [engine's annotation pages](/docs/engine/annotations)
holds here too.

## The built-in tools

| Tool                                              | Makes                                       | How                                   |
| :------------------------------------------------ | :------------------------------------------ | :------------------------------------ |
| `square`, `circle`                                | A rectangle or an ellipse                   | Drag, or click for a default size     |
| `line`                                            | A line                                      | Drag, or click for a default length   |
| `polygon`, `polyline`                             | A closed or open shape of points            | Click each point, double-click to end |
| `ink`, `ink-highlight`                            | Freehand drawing, or a see-through marker   | Draw                                  |
| `free-text`, `free-text-callout`                  | A text box, or one with a line pointing out | Click or drag, then type              |
| `highlight`, `underline`, `strikeout`, `squiggly` | A mark over text                            | Select text                           |
| `insert-text`, `replace-text`                     | A suggested insertion or replacement        | Select text                           |
| `note`                                            | A sticky note                               | Click                                 |
| `stamp`                                           | An image or a PDF drawing                   | Click, then pick a file               |
| `attachment`                                      | A file pinned to a spot                     | Click, then pick a file               |
| `link`                                            | An area that opens a page or a website      | Drag                                  |

Measurement tools (`distance`, `perimeter`, `area`) and redaction (`redact`) are annotation tools
too. Their plugins add the rest of what they need.

You can change any tool, or add your own; see [Tools & defaults](annotations/tools).

## Who wrote it

Every annotation records who made it and who last changed it (`author`, `createdAt`,
`modifiedAt`). You don't set these on the annotation: they come from the user the document was
opened for.

<Engine local>

Tell the viewer who the user is, once. Every document it opens uses it:

<Example name="annotation/identity" />

> **Try it:** draw a rectangle. The list says Dana Smith made it, and when.

<Snippet name="annotations/index/identity" />

`displayName` becomes the `author` other PDF apps show, and `userId` records who owns the
annotation. The same identity decides which annotations count as the user's own when
[permissions](concepts/permissions) say "only their own".

To open one document as someone else, give its entry in `initialDocuments` its own `identity`. It
replaces the viewer's for that document:

```ts
const initialDocuments = [
  { source: contract, identity: { userId: 'bot', displayName: 'Import job' } },
];
```

A document keeps the identity it was opened with. <Fw only={['react', 'vue', 'svelte']}>If you change the viewer's `identity`, documents
you open afterwards use the new one.</Fw>

<Fw only="angular">

To change the viewer's identity, for example when someone else signs in, use
`inject(EpdfViewer).updateSettings({ identity })`. Documents you open afterwards use the new one. To
follow your app's login:

```ts
effect(() => viewer.updateSettings({ identity: auth.identity() }));
```

</Fw>

</Engine>

<Engine cloud>

It comes from the identity in the user's token; see
[Authentication](/docs/engine/authentication#what-a-token-carries).

</Engine>

## Settings

Set them when you register the plugin. `snap`, `chrome`, `afterCreate` and `editable` can change
while the app runs, with `annotation.updateSettings({ … })`; `annotation.resetSettings()` goes back to what you
registered, and `useAnnotationSettings()` gives you the current settings.

| Setting       | What it does                                                          | Page                                                        |
| :------------ | :-------------------------------------------------------------------- | :---------------------------------------------------------- |
| `tools`       | Change the built-in tools, or add your own                            | [Tools & defaults](annotations/tools)                       |
| `afterCreate` | Select a new annotation? Keep the tool active?                        | [What happens after drawing](annotations/tools#what-happens-after-drawing) |
| `chrome`      | The selection's outline, handles and rotation handle, and their colors | [Change the handles](annotations/selecting#change-the-handles) |
| `snap`        | Snapping to other annotations and to angles                           | [Snapping](annotations/selecting#snapping)                  |
| `editable`    | Which annotations people may change by hand: `true`, `false`, or kinds | [What people may change](annotations/selecting#what-people-may-change) |

Every color in `chrome` can also come from CSS, for dark mode or a different look per viewer: see
[Colors & theming](ui/theming).

## Next steps

<Cards>
  <Card
    title="Annotations in code"
    description="Read, create, change and delete annotations from your own code."
    href="annotations/with-code"
  />
  <Card
    title="Events, saving & sharing"
    description="Listen for changes, save them into the PDF, or move them to your own server."
    href="annotations/saving"
  />
  <Card
    title="Tools & defaults"
    description="Default colors, your own tools, the cursor, and a preview before placing."
    href="annotations/tools"
  />
  <Card
    title="Selecting & editing"
    description="Selection, a menu, a style panel, and your own handles."
    href="annotations/selecting"
  />
  <Card
    title="Your own annotation look"
    description="Draw an annotation with your own component, and show cards on hover."
    href="annotations/custom-rendering"
  />
  <Card
    title="Comments & replies"
    description="Build a comments sidebar with threads, replies and review states."
    href="annotations/comments"
  />
</Cards>

---

### Website Src Content Docs Headless Forms Index (`website/src/content/docs/headless/forms/index.mdx`)

---
title: Filling forms
description: Let people fill in PDF forms, read and set values from your code, check required fields, and move form data in and out.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

# Filling forms

Many PDFs are forms: text boxes, checkboxes, dropdowns, signature fields. Register `formPlugin()` and
put `<FormLayer>` in each page, and people can fill them in:

<Snippet name="forms/form-layer" />

<Example name="form/basic" />

Each field becomes a real HTML control, placed and styled like the field in the PDF: people type in text
boxes, tick checkboxes, pick from lists, and move between fields with Tab. A value is saved into the
document as it's entered, so it's there when the PDF is downloaded, and every PDF app shows it. The
form layer paints the fields as the engine draws them, so while it's on the page, the render layer
leaves them out of the page picture.

## Read the form

<Fw only={['react', 'vue', 'svelte']}>

`useFormState()` gives you every field, and <Word of="rerenders" /> when a value changes:

</Fw>

<Fw only="angular">

`form.fields()` is every field, as a signal: your view updates when a value changes.

</Fw>

<Snippet name="forms/field-list" />

Each field has its full `name` (such as `billing.name`), its `family` (`'text'`, `'checkbox'`,
`'radio'`, `'combobox'`, `'listbox'`, `'pushbutton'` or `'signature'`), its value, and a `ref`: its
address for everything else. The fields and their values are the engine's; see
[Forms](/docs/engine/forms#read-the-form) for every family's value.

For one field, such as a total you show in your own UI, use `useFormValue()`:

<Snippet name="forms/invoice-total" />

`toFieldRef(name)` makes a ref from a field's full name, for when you know a field by name.

<Example name="form/read" />

## Fill in a field

`setValue()` takes the same values a field reads:

```ts
await form.setValue(name.ref, { value: 'Ada Lovelace' }); // text
await form.setValue(agree.ref, { checked: true }); // checkbox
await form.setValue(plan.ref, { value: 'yearly' }); // radio group: the button's export value
await form.setValue(country.ref, { value: 'NL' }); // dropdown: an option's value
await form.setValue(toppings.ref, { selectedValues: ['cheese', 'olives'] }); // list
```

It runs the same checks and scripts as typing does, and resolves with `{ field, status }`: `status`
is `'applied'`, `'unchanged'`, or `'rejected'` when the form's own validation refused the value.

The field shows the new value at once, before the engine answers, and the promise resolves once it
has. What the form's scripts calculate from it goes with it, as one change: one request, and one
step to undo. The field stays editable meanwhile; filling it in again is the next change. A value
the engine refuses goes back to the one the field had.

<Fw only="angular">

Your own inputs can fill in fields too. `form.controlOf(ref)` gives you a field as an Angular
`FormControl`: it holds the field's value, and fills in the field when the control changes, with
the same checks and scripts:

<Snippet name="forms/form-control" />

</Fw>

To fill many fields at once, such as from your backend, pass them by name:

```ts
await form.importValues({ 'billing.name': 'Ada Lovelace', 'billing.country': 'NL', agree: true });
```

`importValues()` and `setValues()` write every value as one change, with the calculations run once
after them. Each field's own keystroke and validation scripts don't run, as when a script sets a
field's `value` in Acrobat.

<Example name="form/fill" />

## Check required fields

A form marks some fields as required. Check them before you submit or download:

```ts
const { valid, missing } = form.validate();

const widget = !valid && form.getWidget(missing[0].widgets[0]);
if (widget) stage.reveal(widget.page, { rect: widget.rect });
```

`missing` lists the required fields that are empty, in page order, so you can take the user to the
first one. Only the fields the user may fill in count: another signer's are theirs to fill.

<Example name="form/validate" />

## Calculations, formatting and validation

Many forms add up totals, format dates and check values with scripts. With JavaScript on in the
[actions plugin](documents/actions), they run as people type, as in Acrobat:

<Snippet name="forms/scripts" />

When a script refuses a value, such as a date in the wrong format, the field keeps its old value,
the box keeps what was typed so it can be corrected, as in Acrobat, and
<Fw only={['react', 'vue', 'svelte']}>`onValidationRejected`</Fw><Fw only="angular">`validationRejected$`</Fw> tells you why, so you can show a message.

The scripts read the form as the user sees it, values still on their way included, so nothing is
read from the engine for them. Their engine starts when the first field of a form with scripts takes
focus, so the first value doesn't wait for it; a form without scripts never starts it.

## Reset the form

```ts
await form.reset(); // every field the user may fill in, back to its default
await form.reset([name.ref, email.ref]); // some fields
```

## Move form data in and out

For your own backend, plain values are simplest:

```ts
const values = form.exportValues(); // { 'billing.name': 'Ada Lovelace', agree: true, … }
await form.importValues(values);
```

Fields are matched by their full name. A field the form doesn't have, or a value it can't take, is
skipped; the rest still land.

To copy the fields themselves to another document, with their boxes, values and scripts, take
them out as a bundle and import it there. `FormTransfer` turns a bundle into one JSON text to
store, and back:

```ts
const bundle = await form.export(); // every field; or form.export({ fields }), ({ pages })
const text = FormTransfer.stringify(bundle);

const { fields, dropped } = await otherForm.import(await FormTransfer.parse(text));
```

The import adds the fields as one change you can undo. A field whose name the form has already is
left out and listed in `dropped`. By default each field keeps who created and filled it in, which
needs `canRestoreAttribution()`; pass `{ attribution: 'stamp' }` to make them yours. See
[Import & export](/docs/engine/forms/import-export).

<Example name="form/data" />

## Methods

What `useForm()` gives you for reading and filling. The verbs that change the form itself are
on [Building forms](forms/building#methods).

| Method                     | What it does                                                                                                                                   |
| :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- |
| `get(ref)`                 | One field, with its value, or `null` when the form has no such field. [Read the form](#read-the-form)                                           |
| `getValue(ref)`            | One field's value, in the shape `setValue()` takes, or `null`. In a component, use `useFormValue(ref)`.                                        |
| `getFieldForWidget(widget)` | The field a box belongs to, from the box's ref                                                                                                |
| `getWidget(widget)`        | A box's row: its page, its place (`rect`), its look and its state, or `null` when no page shows it                                             |
| `getWidgets(ref)`          | Every box a field shows in, as rows in the field's order: a radio group's buttons, or a field repeated on several pages                       |
| `listWidgets(page)`        | Each field box on a page, with its place, its control, its value and whether it can be filled. For drawing the fields yourself.                |
| `getWidgetAt(page, point)` | The field box under a point on a page, with its field, or `null`                                                                               |
| `setValue(ref, value)`     | Fills in a field with `{ value }`, `{ checked }` or `{ selectedValues }`, running the form's checks and scripts. Resolves `{ field, status }`. [Fill in a field](#fill-in-a-field) |
| `setValues(entries)`       | Fills in several fields, each `{ ref, value }`, as one change with one recalculation; a field you may not fill in, or a value it can't take, fails on its own. Resolves `{ applied, skipped, failed }`. |
| `importValues(values)`     | Fills in fields by full name from a plain object, as one change with one recalculation. Resolves `{ applied, skipped, failed }`. [Move form data in and out](#move-form-data-in-and-out) |
| `exportValues()`           | Every value, keyed by full name, as a plain object                                                                                             |
| `export(selection?)`       | Whole fields with their boxes, values and scripts, as a bundle: every field, or `{ fields }` or `{ pages }`. [Move form data in and out](#move-form-data-in-and-out) |
| `import(bundle, options?)` | Adds a bundle's fields, as one change. Resolves `{ fields, refMap, dropped }`. Needs `canDesign()`. [Move form data in and out](#move-form-data-in-and-out) |
| `reset(refs?)`             | Puts back each field's default, for every field the user may fill in or the fields you pass. Resolves `{ fields }`, the fields it changed. [Reset the form](#reset-the-form) |
| `validate()`               | Checks the required fields the user may fill in. Returns `{ valid, missing }`, the empty ones in page order. [Check required fields](#check-required-fields) |
| `activateWidget(widget)`   | Does what a click on a field box does, such as running a button's action                                                                       |
| `getSnapshot()`            | Everything at once, as the user sees it: what the engine's `list()` returns, with the values still on their way. `fields`, `widgets`, `formKind`, `needsAppearances` and `calculationOrder` |
| `refresh()`                | Reads the whole form again, and <Fw only={['react', 'vue', 'svelte']}>`onResynced`</Fw><Fw only="angular">`resynced$`</Fw> fires. You rarely need it: the plugin follows every change, whoever made it.                      |
| `canRead()`, `canFill(ref)` | Whether reading the form, and filling in a field, are allowed. [Permissions](#permissions)                                                   |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()` | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use `useFormSettings()`. [Settings](#settings) |

Every method that returns a promise also takes a `signal` in its options, to cancel it.

## State

<StateIntro hook="useFormState()" />

| Field           | What it is                                                                                      | Read once                                            |
| :-------------- | :---------------------------------------------------------------------------------------------- | :--------------------------------------------------- |
| `fields`        | Every field, with its value                                                                     | `list()`, or `list({ family, page, name })` for some |
| `status`        | Whether the form is read: `'idle'`, `'loading'`, `'ready'`, `'forbidden'` or `'error'`           | `getStatus()`                                        |
| `formKind`      | What the document has: `'none'`, `'acroform'` (a regular PDF form) or `'xfa'`                   | `getFormKind()`                                      |
| `selectedField` | In design mode, the field of the selected box, or `null`; see [Building forms](forms/building#change-a-fields-settings) | `getSelectedField()`          |

For one field, `useFormValue(ref)` <Word of="rerenders" /> only when that field's value changes.

## Settings

Set them when you register the plugin, and change them while the app runs with
`form.updateSettings({ … })`; `form.resetSettings()` goes back to what you registered, and
`useFormSettings()` gives you the current settings.

| Setting             | What it does                                                               | Default                      | From CSS                       |
| :------------------ | :------------------------------------------------------------------------- | :--------------------------- | :----------------------------- |
| `validation`        | Run the form's own checks (`'scripts'`), or accept every value (`'none'`)  | `'scripts'`                  | —                              |
| `focus.color`       | The ring around the field being filled in                                  | the viewer's accent          | `--epdf-form-focus`            |
| `fields.border`     | The edge of a field that has no border of its own                          | the viewer's accent at 55%   | `--epdf-form-field-border`     |
| `fields.background` | Inside a field that has no background of its own                           | `'rgb(255 255 255 / 0.92)'`    | `--epdf-form-field-background` |
| `fields.color`      | The check mark and text of a field that has no color of its own            | `'#1f2a44'`                    | `--epdf-form-field-color`      |
| `fillable`          | Which fields people fill in by hand: `true`, `false`, or field families    | `true`                       | —                              |

Scripts need JavaScript on in the actions plugin; without it, `'scripts'` accepts every value too,
and a form with scripts logs a hint once.

`fillable` is for the people using your app. A field it leaves out draws as a read-only field
does, and takes no typing, clicks or picks; your code still fills it in with `setValue()` and
`importValues()`, and `canFill(ref)` doesn't change:

```ts
formPlugin({ fillable: false }); // show the values, take no input
form.updateSettings({ fillable: ['text', 'checkbox'] }); // only these families
form.updateSettings({ fillable: true }); // every field again
```

The families are `'text'`, `'checkbox'`, `'radio'`, `'combobox'`, `'listbox'`, `'pushbutton'` and
`'signature'` (signing a field is filling it in). To stop your code too, leave `doc.forms.fill` out
of the scope (see [Permissions](#permissions)).

The colors are what the viewer draws around fields; a field's own colors are saved in the PDF (see
[Building forms](forms/building)). A CSS variable wins over the setting: see
[Colors & theming](ui/theming).

<Example name="form/colors" />

## Events

<Snippet name="forms/autosave" />

| Event                  | Fires when                                                                | Carries           |
| :--------------------- | :------------------------------------------------------------------------ | :---------------- |
| `onValueChanged`       | A field's value changed                                                   | `field`, `origin` |
| `onValidationRejected` | The form's validation refused a value                                     | `field`, `issues` |
| `onResynced`           | The whole form was read, as when it opens, after `refresh()` or a repair  | `snapshot`        |
| `onSettingsChanged` | The settings changed | `settings`, `changed` |

Fields that are added, changed or removed have their own events; see
[Building forms](forms/building#events).

## Permissions

| Permission       | What it allows   | Check               |
| :--------------- | :--------------- | :------------------ |
| `doc.forms.read` | Reading the form | `form.canRead()`    |
| `doc.forms.fill` | Filling it in    | `form.canFill(ref)` |

Without it, the fields show their values but can't be changed. A user with a `fields:fill`
permission for one group fills in that group's fields only: `form.canFill(ref)` answers per field,
the other fields can't be changed and Tab skips them, `validate()` leaves them out, and `reset()`
leaves them as they are. See [Signers and groups](/docs/engine/forms/groups).

## Good to know

### Forms and annotations

A field's widgets are the form's: the annotation plugin never lists them. With the
[annotation plugin](annotations) registered, the `<FormLayer>` and the annotation layer share the page: fields stay fillable with the
`pointer` tool, and become movable boxes in [design mode](forms/building).

### Keyboard

Tab moves to the next field in the page's order. In a text box, Enter confirms and Escape puts back
the value it had.

### Collecting responses

<Engine local>

On the local engine, the values live in the document in this tab until it's downloaded.

<CloudPdfCallout
  title="Collect form responses on your server"
  href="https://www.cloudpdf.com/docs/headless/forms"
  cta="Collect forms with CloudPDF"
>

With CloudPDF, each value is stored on your server as it's entered, so a half-filled form survives a
closed tab, and your backend reads the answers without asking the user to download or upload a
file.

</CloudPdfCallout>

</Engine>

<Engine cloud>

Each value is stored on your server as it's entered, so a half-filled form survives a closed tab.

</Engine>

### XFA forms

Some PDFs also carry a form in the older XFA format. The viewer works with the document's regular
fields and never runs XFA; see [Forms](/docs/engine/forms#xfa-forms).

### Signed documents

A signature can lock the whole form, or some fields. Locked fields show their values and can't be
changed; see [Signatures](forms/signatures).

---

### Website Src Content Docs Viewer Features Index (`website/src/content/docs/viewer/features/index.mdx`)

---
title: All features
description: Everything the viewer can do, how to leave a feature out, who the user is, and where each feature's settings go.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

# All features

Every feature is on from the start. Leave out the ones you don't need with `disable`, by the name
in the first column:

```js
{ disable: ['forms', 'redaction', 'measurement'] }
```

| Feature | What people can do | Settings |
| :-- | :-- | :-- |
| `annotations` | Highlight, underline and strike out text; draw; add notes, text boxes and callouts | [Annotations](features/annotations) |
| `shapes` | Draw rectangles, circles, lines, arrows and polygons | [Annotations](features/annotations#the-tools) |
| `comments` | Comment on annotations and reply, in a panel with every thread | [Annotations](features/annotations#comments) |
| `stamps` | Place stamps: the standard ones, their own, or images | [Annotations](features/annotations#stamps) |
| `signatures` | Draw, type or upload a signature; sign fields; check signatures | [Forms & signatures](features/forms-and-signatures#signatures) |
| `forms` | Fill in forms; build them in the Form mode | [Forms & signatures](features/forms-and-signatures) |
| `redaction` | Mark text and areas, then remove them from the file for good | [Annotations](features/annotations#redaction) |
| `measurement` | Measure distances, perimeters and areas at the drawing's scale | [Annotations](features/annotations#measurements) |
| `pages` | Rotate, move, insert, duplicate and delete pages in the thumbnails panel, each step undoable | [Panels](customize/panels#organize-pages) |
| `attachments` | See, save, attach and remove files inside the PDF | [Panels](customize/panels#the-attachments) |
| `search` | Find text, with every match highlighted and listed | [`plugins.search`](/docs/headless/text/search#settings) |
| `text` | Select and copy text | [`plugins.selection`](/docs/headless/text/selection#settings) |
| `thumbnails` | A picture of every page, to jump around | [Panels](customize/panels) |
| `outline` | The document's table of contents | [Panels](customize/panels) |
| `documents` | Open other files (with the local engine), download, print, several documents in tabs | [Documents](documents) |

## Leave features out

A feature in `disable` is gone everywhere at once: its buttons, its mode tab, its panel, its menu
rows and its shortcuts. Annotations already in the document still show; people just can't add new
ones. Most features have parts you can turn off on their own, such as `forms.building` or
`documents.download`, and one button goes by its command's name, such as `tool:highlight`.

`disable` follows its prop, so a feature can come and go while the viewer runs, for example when
a user's plan changes.

[Turn features off](features/turning-off) has every name, read-only, and what to use when: the
layout for where something goes, `disable` for whether the viewer has it, and the scope for what a
user may do.

## Who the user is

<Engine local>

The user's name becomes the author of the annotations and comments they make:

```js
{ identity: { displayName: 'Ada Lovelace' } }
```

</Engine>

<Engine cloud>

The user's name becomes the author of the annotations and comments they make. On CloudPDF, who the
user is and what they may do come from the document's token, so your server decides.

</Engine>

## Each feature's settings

Every feature is a headless plugin, and `plugins` takes its settings by the plugin's name, exactly
as the plugin's page lists them:

```js
{
  plugins: {
    stage: { spread: 'odd', zoom: { mode: 'fit-width' } },
    search: { highlight: { color: '#a7f3d0' } },
    annotation: { tools: [{ id: 'highlight', defaults: { color: '#fde047' } }] },
  },
}
```

That opens documents as two-page spreads at the page's width, with green search matches and
yellow highlights. See [Plugins](code/plugins) for every plugin and how to change its settings
while the viewer runs.

## Good to know

### What loads when

A feature you disable is still in the viewer's code: `disable` changes what people see, not what
you ship. The first load stays small another way. Each feature's panels and dialogs are files of
their own, which the viewer fetches in the background once the document shows, so they open at
once. The heavier parts wait until they're used: the signature checks until a signature is
checked, the standard stamps until Stamps opens, the handwriting fonts until someone types a
signature, and each language until it's chosen. [How it works](concepts/how-it-works#what-loads-when)
has the whole order.

---

### Cloudpdf Website Src Content Docs Engine Annotations Index (`cloudpdf/website/src/content/docs/engine/annotations/index.mdx`)

---
title: Annotations
description: List, add, change, reorder and delete annotations, with the fields and styles every kind shares.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

import { Callout } from '@/components/docs/callout';
import { CardGrid, GridCard } from '@/components/docs/cards';

# Annotations

Annotations are what people add on top of a page: highlights, sticky notes, shapes, stamps,
free text, links. They're stored in the PDF, so every PDF app shows them, and they stay separate
from the page's own content, so you can change or remove them later.

```ts
const page = doc.page(pageRef);

const { annotation } = await page.annotations.create({
  subtype: 'square',
  box: { x: 72, y: 92, width: 200, height: 100 },
  color: '#0078ff',
});

await page.annotations.update(annotation.ref, { contents: 'Check this table' });
await page.annotations.delete(annotation.ref);
```

## Kinds

Each kind has its own page with an example and its fields:

<CardGrid columns={2}>
  <GridCard
    icon="annotation"
    title="Highlights & text markup"
    description="Highlight, underline, squiggly and strikeout over text."
    href="/docs/engine/annotations/text-markup"
  />
  <GridCard
    icon="note"
    title="Notes & replies"
    description="Sticky notes, their popups, and threads of replies."
    href="/docs/engine/annotations/notes-and-replies"
  />
  <GridCard
    icon="free-text"
    title="Free text"
    description="Text boxes and callouts, with fonts and rich text."
    href="/docs/engine/annotations/free-text"
  />
  <GridCard
    icon="shape"
    title="Shapes & drawing"
    description="Rectangles, ellipses, lines, polygons and freehand ink."
    href="/docs/engine/annotations/shapes"
  />
  <GridCard
    icon="stamp"
    title="Stamps"
    description="Images and drawings placed on a page."
    href="/docs/engine/annotations/stamps"
  />
  <GridCard
    icon="link"
    title="Links"
    description="Areas that go to another page or open a website."
    href="/docs/engine/annotations/links"
  />
  <GridCard
    icon="attachment"
    title="File attachments"
    description="A file pinned to a spot on the page."
    href="/docs/engine/annotations/file-attachments"
  />
  <GridCard
    icon="measurement"
    title="Measurements"
    description="Distances, perimeters and areas at a real-world scale."
    href="/docs/engine/annotations/measurements"
  />
</CardGrid>

## List annotations

Every annotation in the document, or the ones on one page:

```ts
const { annotations } = await doc.annotations.list(); // every page
const { annotations } = await doc.annotations.list({ pages: [pageA, pageB] }); // some pages
const { annotations } = await doc.page(pageRef).annotations.list(); // one page
```

A list is cheap even for a whole document: the engine reads the annotations without loading the
pages. Each annotation names its `page`, and the list's `pages` has one entry per page it read.

Each annotation has a `subtype` (its kind), a `rect` (where it is on its page) and a `ref` (its
address). Keep the `ref`: it's how you change or delete the annotation later.

Positions are in points from the top-left corner of the page, with y going down, like every
position in the engine: a box is `{ x, y, width, height }` and a point `{ x, y }`. See
[Coordinates](/docs/engine/concepts/coordinates).

```ts
for (const annotation of annotations) {
  console.log(annotation.subtype, annotation.rect, annotation.contents);
}
```

A list includes kinds the engine can't create yet: they come back as `subtype: 'unsupported'`,
with their position and common fields. They stay in the PDF untouched.

Form widgets are annotations too, of kind `'widget'`, but they're the form's: a list here never
holds one, and `doc.forms.list()` returns them with the fields. You place and change them through
the form; see [Creating form fields](/docs/engine/forms/authoring).

## Fields every annotation has

Besides `subtype`, `rect` and `ref`, every kind has these fields:

| Field      | What it is                                                                                               |
| :--------- | :------------------------------------------------------------------------------------------------------- |
| `contents` | A comment, the text PDF apps show for the annotation.                                                    |
| `subject`  | A short title PDF apps show with the comment, such as `'Question'`.                                      |
| `nm`       | A unique name on its page. See [Refs & identity](/docs/engine/concepts/identity).                        |
| `reply`    | The annotation it answers, or `null`. See [Notes & replies](/docs/engine/annotations/notes-and-replies). |
| `popup`    | The ref of its popup window, or `null`. See [Popups](/docs/engine/annotations/notes-and-replies#popups). |
| `groupId`  | The group it belongs to, when you work with groups.                                                      |

Each annotation also has the [flags](#flags) and the fields that say [who wrote it](#who-wrote-it).

## Add an annotation

`create()` takes the kind and its fields, and returns the new annotation:

```ts
const { annotation } = await page.annotations.create({
  subtype: 'circle',
  box: { x: 60, y: 392, width: 120, height: 100 },
  interiorColor: '#1e90ff',
});
```

Fields you leave out get the kind's default. The engine draws the annotation's appearance for
you, so it shows the same in every PDF app.

Stamps and file attachments also carry bytes (an image, a file). Pass those in a second
argument; see [Stamps](/docs/engine/annotations/stamps) and
[File attachments](/docs/engine/annotations/file-attachments).

## Change an annotation

`update()` takes the `ref` and only the fields you want to change:

```ts
await page.annotations.update(annotation.ref, {
  color: '#dc143c',
  contents: 'Reviewed',
});
```

A field you leave out keeps its value. A field set to `null` is removed, for example
`{ interiorColor: null }` for no fill. A field's value is replaced whole (a `file`, a list of
points), except rich text's `body` style, which merges.

You can also pass an annotation you read straight back: every field it has is one `update()`
accepts. That makes "change a copy, then save it" code simple:

```ts
await page.annotations.update(annotation.ref, { ...annotation, opacity: 0.5 });
```

Fields the engine writes (`author`, the dates, `userId`, `ref`…) are accepted and ignored. For
free text, send `contents` or `richText`, not both with different text.

### Move or resize by `rect`

An update takes a new `rect` for every annotation, as a script's `annot.rect` does in Acrobat. A
note, a file, a link, a popup and a redaction take it as it is.

Every other annotation is drawn from its shape: rectangles, ellipses, text boxes, stamps, carets
and form fields' widgets from their `box`, lines, polylines, polygons and ink from their points,
and text markup from its quads. For these, a new `rect` puts the shape there: each point is mapped from the old
`rect` onto the new one, each axis on its own. A `rect` of the same size moves the drawing, and a
bigger one stretches it:

```ts
const { rect } = line;

// 40 points right, 20 down
await page.annotations.update(line.ref, {
  rect: { ...rect, x: rect.x + 40, y: rect.y + 20 },
});
```

The engine then works out `rect` again from the shape. After a resize it can differ a little from
the one you sent: a line keeps its width, and its endings keep their size. The `rect` you read,
sent back, changes nothing, so a read passed straight back works.

- A new `rect` with a new shape (`box`, points or quads) fails with `InvalidArg`: send one or the
  other.
- A turned drawing can only move. A `rect` of another size fails with `InvalidArg`; resize its
  `box` or its points instead. A box at a quarter turn is the exception: it stands upright on the
  page, so a `rect` resizes it along its own sides.
- A `rect` that would squash a drawing to no width or no height fails with `InvalidArg`.

`shapeForRect(annotation, rect)` gives the fields that put an annotation at a `rect`, by the same
rule. Use it to place a copy somewhere else:

```ts
import { shapeForRect } from '@cloudpdf/engine';

await page.annotations.create({ ...copy, ...shapeForRect(copy, target) });
```

A create works out a drawn annotation's `rect` from its shape, whatever `rect` you send, so a
copy of another app's drawing keeps its shape exactly. A box turned a quarter can be created by
its `rect` instead of its `box`; see [Turned shapes](/docs/engine/annotations/shapes#turned-shapes).

## Delete and reorder

```ts
await page.annotations.delete(annotation.ref);

await page.annotations.reorder([annotation.ref], 'start'); // to the back
await page.annotations.reorder([annotation.ref], { after: other.ref }); // just above another
```

Deleting an annotation also deletes its popup, if it has one, and its replies; see
[Delete a note](/docs/engine/annotations/notes-and-replies#delete-a-note).

Annotations on a page are drawn in order, so the last one is on top. `reorder(refs, position)`
moves annotations together, in the order you pass them, right after or before another annotation
of the page, or to the back (`'start'`) or the front (`'end'`). It resolves the page's new order,
`order`, as refs, and fires `annotations.reordered`.

Widgets are drawn above every annotation, in an order of their own, so a widget is never a
neighbour here: `reorder()` refuses one with `InvalidArg`. Restack widgets with
[`doc.forms.reorderWidgets()`](/docs/engine/forms/authoring#move-and-style-widgets).

## Style

Most kinds share the same style fields:

| Field           | What it is                                              | Default   |
| :-------------- | :------------------------------------------------------ | :-------- |
| `color`         | Line color, such as `'#1e90ff'`                         | per kind  |
| `interiorColor` | Fill color of closed shapes, or `null` for no fill      | no fill   |
| `opacity`       | From `0` (invisible) to `1` (solid)                     | `1`       |
| `strokeWidth`   | Line width in points                                    | `1`       |
| `borderStyle`   | `'solid'` or `'dashed'`                                 | `'solid'` |
| `dashArray`     | The pattern a `'dashed'` border draws, such as `[3, 2]` | `[3]`     |

A pattern alone doesn't make a border dashed: a `'solid'` border keeps its `dashArray` and draws
solid. `dashArray: null` on a dashed border draws the default `[3]`, 3 points on and 3 off. A
border from another app can read `'beveled'` or `'inset'`; only form widgets draw those, so a
write takes them only sent back unchanged.

Colors are `'#rrggbb'`, as in CSS: the engine gives them in lowercase and takes either case. They
never include transparency: that's what `opacity` is for. The default color depends on the kind
(yellow for a highlight, red for a shape, black for free text); each kind's page lists its
defaults.

A PDF can store a color as gray or CMYK. It reads as the same color in `'#rrggbb'`, and sent back
unchanged it stays as the file has it, so passing a read straight back rewrites nothing.

## Who wrote it

The engine records who created and last changed each annotation, from the user the document was
opened for. You read these fields; you don't set them:

| Field        | What it is                                          |
| :----------- | :-------------------------------------------------- |
| `author`     | The display name shown in PDF apps.                 |
| `createdAt`  | When it was created, as an ISO date.                |
| `modifiedAt` | When it was last changed.                           |
| `userId`     | The id of the user who owns it.                     |
| `createdBy`  | The id of the user who created it.                  |
| `modifiedBy` | The id of the user who last changed it.             |
| `groupId`    | The group it belongs to, when you work with groups. |

<Engine local>

The user comes from the `identity` you pass when you open the document; see
[Opening documents](/docs/engine/documents/opening#who-is-working-on-it).

</Engine>

<Engine cloud>

The user comes from the identity in their token; see
[Authentication](/docs/engine/authentication#what-a-token-carries).

</Engine>

## Flags

Every annotation has the standard PDF flags as `true`/`false` fields. Set the ones you need; the
rest stay off (on create) or keep their value (on update). The exception is `print`: a new
annotation prints unless you set `print: false`, as in Acrobat. A popup doesn't print.

```ts
await page.annotations.update(annotation.ref, { print: true, locked: true });
```

| Flag             | What it does                                            |
| :--------------- | :------------------------------------------------------ |
| `print`          | Printed with the page. Off: shown on screen only.       |
| `hidden`         | Not shown and not printed.                              |
| `noView`         | Not shown on screen (can still print).                  |
| `locked`         | Can't be moved, resized or deleted in PDF apps.         |
| `lockedContents` | Its text can't be changed in PDF apps.                  |
| `readOnly`       | Can't be changed at all in PDF apps.                    |
| `noZoom`         | Keeps its size when the page is zoomed (notes, icons).  |
| `noRotate`       | Stays upright when the page is rotated.                 |
| `invisible`      | Hidden when the app doesn't know the annotation's kind. |
| `toggleNoView`   | Flips `noView` when the pointer is over it.             |

Flags are instructions for PDF apps. The engine itself still lets you change a locked
annotation; the viewer's annotation tools respect the flags in their interface.

## Permissions

Reading annotations needs `doc.annotate.read`. Creating, changing and deleting them needs a
permission for that action, which can be limited to the user's own annotations:
`annotations:update:self` lets a user edit what they created and nothing else. See
[Permissions](/docs/engine/concepts/permissions#annotations).

<Engine local>

<CloudPdfCallout
  title="Let many people annotate one document"
  href="https://www.cloudpdf.com/docs/engine"
  cta="Collaborate with CloudPDF"
>

With CloudPDF, annotations are stored on your server as people make them. Everyone with the
document open gets each other's changes as [events](/docs/engine/concepts/events), and the server
makes sure people only change the annotations they may change.

</CloudPdfCallout>

</Engine>

## More

- [Import & export](/docs/engine/annotations/import-export): copy annotations to another
  document, or store them apart from the PDF.
- [Drawing annotations yourself](/docs/engine/annotations/appearances): render annotations as
  separate images, bake them into the page, or export them as a PDF.
- [Refs & identity](/docs/engine/concepts/identity): what a `ref` contains, and why you never
  build one.
- [Changes & undo](/docs/engine/documents/changes): make several annotation edits as one action,
  and undo them.

---

### Cloudpdf Website Src Content Docs Engine Forms Index (`cloudpdf/website/src/content/docs/engine/forms/index.mdx`)

---
title: Forms
description: Read a PDF form's fields and values, fill them in, reset them, and move fields and values between documents.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

# Forms

A PDF form is a set of fields: text boxes, checkboxes, radio buttons, dropdowns and lists. The
engine reads every field with its value, fills fields in, and redraws them so the new value shows
in every PDF app.

```ts
const { fields } = await doc.forms.list();

const name = fields.find((field) => field.name === 'billing.name')!;
await doc.forms.setValue(name.ref, { value: 'Ada Lovelace' });
```

## Read the form

`list()` returns every field in the document:

```ts
const { fields } = await doc.forms.list();

for (const field of fields) {
  console.log(field.name, field.family);
}
```

Every field has these:

| Field           | What it is                                                                                                                          |
| :-------------- | :---------------------------------------------------------------------------------------------------------------------------------- |
| `name`          | The full name, with the groups it's in: `billing.name`.                                                                             |
| `family`        | `'text'`, `'checkbox'`, `'radio'`, `'combobox'`, `'listbox'`, `'pushbutton'`, `'signature'` or `'unknown'`.                         |
| `ref`           | Its address. Keep it: it's how you fill, reset or change the field.                                                                 |
| `readOnly`      | `true` when PDF apps don't let the user change the value.                                                                           |
| `required`      | `true` when PDF apps ask for a value before the form is submitted.                                                                  |
| `noExport`      | `true` when the field is left out when the form is submitted.                                                                       |
| `alternateName` | The tooltip PDF apps show, also read by screen readers, or `null`.                                                                  |
| `groupId`       | The group the field belongs to: who fills it in, such as `'buyer'`, or `null`. See [Signers and groups](/docs/engine/forms/groups). |
| `widgets`       | Where the field shows on the pages (see [Widgets](#widgets)).                                                                       |

A document without a form returns an empty `fields`. The list's `formKind` says what the document
has: `'none'`, `'acroform'` (a regular PDF form) or `'xfa'` (see [Good to know](#xfa-forms)).

The list also has `needsAppearances`, `true` when the form asks PDF apps to draw its fields
themselves ([`repair()`](/docs/engine/forms/authoring#repair-a-broken-form) can clear it), and
`calculationOrder` (see [Calculations](#calculations-formatting-and-validation)).

### Values

Each family has its own value fields. Check `family` first, and TypeScript knows which ones the
field has:

```ts
if (field.family === 'text') console.log(field.value); // string
if (field.family === 'checkbox') console.log(field.checked); // boolean
if (field.family === 'listbox') console.log(field.selectedValues); // string[]
```

| Family       | Its value                                                          | Also                                                                                             |
| :----------- | :----------------------------------------------------------------- | :----------------------------------------------------------------------------------------------- |
| `text`       | `value`, a string                                                  | `defaultValue`, `maxLength` (`null`: no limit), `multiline`, `password`, `comb`                  |
| `checkbox`   | `checked`                                                          | `exportValue`: what the form data holds while it's checked                                       |
| `radio`      | `value`: the export value of the chosen button, or `'Off'`         | `noToggleToOff`, `radiosInUnison`                                                                |
| `combobox`   | `value`                                                            | `options`, `edit` (free text allowed), `defaultValue`                                            |
| `listbox`    | `selectedValues`, in option order                                  | `options`, `multiSelect`, `defaultValue` (option values), `topIndex` (the option in its top row) |
| `pushbutton` | none: a button only runs an action                                 | Each widget's `caption`, on its row in `widgets`                                                 |
| `signature`  | none here: see [Digital signatures](/docs/engine/forms/signatures) |                                                                                                  |

Each option of a dropdown (`combobox`) or list is `{ label, value, selected }`. `label` is what the
user sees; `value` is what the form stores, and what you pass when you fill the field in.

### Who created and filled it in

A field records who created it and who last filled it in, from the identity the document was opened
with:

```ts
field.createdBy; // 'user-17': who created the field, or null
field.filledBy; // 'user-42': whose fill last changed the value
field.filledByName; // 'Ann de Vries'
field.filledAt; // '2026-10-08T10:42:00Z'
```

| Field                                  | What it is                                                                                                                                                                            |
| :------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `createdBy`, `createdAt`               | Who created the field, and when. `null` for a field another app made.                                                                                                                 |
| `filledBy`, `filledByName`, `filledAt` | The user whose fill last changed the value, their name, and when. A calculated value counts as filled in by whoever's fill calculated it. `null` after a reset, or an anonymous fill. |
| `importedBy`                           | Who imported the field with its attribution kept.                                                                                                                                     |

The engine stamps these; a write can't set them. A fill that leaves the value as it was changes
none of them, and undoing a fill puts the previous ones back. A widget has no attribution of its own:
its field's is the one.

### Widgets

A field shows on a page through a **widget**: the box you see and click. Most fields have one. A
radio group is one field with a widget per button, and a field repeated on every page (a
reference number in the header, say) has a widget per page, all showing the same value.

Each widget in `field.widgets` has its `page` and its `ref`. The form also lists every widget in
`widgets`: where it is (`rect`, in points from the page's top-left), how it looks, its state,
what it does when clicked (`actions`), and the ref of the field it belongs to (`field`). So one read is enough to draw your own form layer
over a page:

```ts
const { widgets } = await doc.forms.list();

for (const widget of widgets) drawBox(widget.page, widget.rect, widget.field);
```

Checkbox and radio widgets in `field.widgets` also have their `exportValue` (what the form data
holds when this widget is checked) and `checked`.

A form field's widgets come with the form, never with the annotations: `page.annotations.list()`
doesn't return them, and reading them takes `doc.forms.read`, not `doc.annotate.read`. Each page's
widget images, every look and state, come from `page.forms.renderAppearances()`.

## Find a field

The `ref` from `list()` is the best address: it keeps working when the field is renamed. When you
know a field by its name instead (from your backend, or from data you stored), make a ref from the name:

```ts
import { toFieldRef } from '@cloudpdf/engine';

const field = await doc.forms.get(toFieldRef('billing.name'));
```

The name is the full name, with dots between the groups. A field that isn't there fails with
`NotFound`.

## Fill in a field

`setValue()` takes the field's `ref` and its new value, in the same fields a read returns. What
you read, you can write back:

```ts
await doc.forms.setValue(name.ref, { value: 'Ada Lovelace' }); // text
await doc.forms.setValue(agree.ref, { checked: true }); // checkbox
await doc.forms.setValue(plan.ref, { value: 'yearly' }); // radio group
await doc.forms.setValue(country.ref, { value: 'NL' }); // dropdown
await doc.forms.setValue(toppings.ref, { selectedValues: ['cheese', 'olives'] }); // list
```

| Family     | Value                                                                           |
| :--------- | :------------------------------------------------------------------------------ |
| `text`     | `{ value }`; `null` empties it                                                  |
| `checkbox` | `{ checked }`                                                                   |
| `radio`    | `{ value }`: the export value of the button to choose; `'Off'` or `null` clears |
| `combobox` | `{ value }`: an option value; `null` clears                                     |
| `listbox`  | `{ selectedValues }`: option values; `[]` clears                                |

`setValue()` returns the field as it is now:

```ts
const { field } = await doc.forms.setValue(name.ref, { value: 'Ada' });
```

The engine redraws the field's widgets, so the page shows the new value in every PDF app. Render
the page again to see it.

### What the engine checks

The engine checks a value before it writes anything. A call that fails with `InvalidArg` leaves
the field as it was:

- The value must be the family's own: `{ checked: true }` on a text field fails.
- A dropdown or list takes option values, not labels. A dropdown with `edit: true` also takes
  free text. More than one value needs a list with `multiSelect`.
- A radio group's `value` must be the export value of one of its buttons.
- A radio group with `noToggleToOff` can't be cleared once a button is chosen.
- Push buttons and signature fields hold no value you can set.

Some things are fixed up rather than refused: text longer than `maxLength` is cut to fit, and
in a radio group with `radiosInUnison`, the buttons that share an export value check together.

`readOnly` is an instruction for PDF apps: they don't let the user change the field. The engine
still accepts your own writes, for example a total your code calculates.

## Reset

`reset()` puts back each field's `defaultValue`, or empties it when it has none. Without an
argument it resets the whole form; pass a ref, or a list of refs, to reset only those fields:

```ts
await doc.forms.reset(); // every field
await doc.forms.reset(name.ref);
await doc.forms.reset([name.ref, email.ref]);
```

It returns the fields it changed, as they are now. A field that already holds its default is
left alone and isn't in the list:

```ts
const { fields } = await doc.forms.reset();

console.log(`${fields.length} fields reset`);
```

Fields that hold no value, such as push buttons and signature fields, are skipped. Resetting the
whole form also skips the fields a signature locked and [fields stored inline](#fields-stored-inline);
name one of those yourself and the call fails, the same as a `setValue()` would.

## Move fields and values between documents

`export()` takes fields out as one bundle, each with its value. `importValues()` fills another
document's fields of the same name from it, and `import()` copies the fields themselves:

```ts
const bundle = await doc.forms.export();

const { fields, dropped } = await other.forms.importValues(bundle);
```

Each value is checked like a `setValue()`. A field the document doesn't have, a value it can't
take, or a field a signature locked is left out and listed in `dropped`; the rest still land, as
one change you can undo. See [Import & export](/docs/engine/forms/import-export).

## Events

`setValue()`, `reset()` and `importValues()` fire `forms.valueSet`, one per field that changed.
Each carries what the call returned for that field:

```ts
doc.events.on('forms.valueSet', ({ field }) => {
  updateSummary(field);
});
```

When one call changes several fields, such as `reset()` or `importValues()`, their events share
`origin.tx`, like an annotation import.

<Engine cloud>

Each value is saved on the server as you set it. Everyone with the document open gets the event,
with `origin.kind: 'remote'` for other people's changes. See
[Events](/docs/engine/concepts/events).

</Engine>

## Permissions

Reading the form needs `doc.forms.read`, and exporting it `doc.download` too. Filling in,
resetting and importing values needs `doc.forms.fill`, which includes `doc.forms.read`. See
[Permissions](/docs/engine/concepts/permissions).

A signed document can take filling away, for the whole form or for single fields. The call then
fails with `ProtectedDocument`; see
[Digital signatures](/docs/engine/forms/signatures#protection-what-signatures-allow).

## More

- [Creating form fields](/docs/engine/forms/authoring): add fields and widgets, change and delete
  them, repair a broken form.
- [Import & export](/docs/engine/forms/import-export): copy fields from one document to another,
  or fill a form in from a bundle.
- [Digital signatures](/docs/engine/forms/signatures): sign a signature field and check the
  signatures in a document.
- [Changes & undo](/docs/engine/documents/changes): fill in several fields as one action, and
  undo it.

## Good to know

### Calculations, formatting and validation

The engine doesn't run the form's JavaScript. `setValue()` stores the value you give it: it
doesn't recalculate a total, format a date or an amount, or run a validation script. PDF apps run
those scripts while a user types.

A field's scripts are in `field.actions` (`keystroke`, `format`, `validate` and `calculate`), and
the list's `calculationOrder` holds the fields whose totals are calculated, in order. To write them,
see [Scripts and calculations](/docs/engine/forms/authoring#scripts-and-calculations).

The viewer's form plugin runs these scripts and writes their results as one
[change](/docs/engine/documents/changes): `forms.setValue`, `forms.reset`, `forms.setDisplay`
(whether the field shows and prints) and `forms.setAppearanceText` (the formatted text the field
shows, while its value stays the same) ops, in the order the scripts asked for them. All of them
are written, or none is: a field a signature locked refuses the whole change. You only need these
ops when you run form scripts yourself.

### Recovered fields

Some PDFs have fields that show on the page but are missing from the form's list of fields, a
bug in the tool that made them. The engine finds them anyway: they come back with
`origin: 'recovered'` and fill in like any other field. Other PDF apps may not see them;
[`repair()`](/docs/engine/forms/authoring#repair-a-broken-form) fixes the file.

### A field on several pages

A field's widgets can be on different pages. The `meta` of a `setValue()` or `reset()` result
lists every widget whose look changed, with its page (`meta.changedWidgets`). For a radio group
that's the button switched off and the one switched on. Use it to render only the pages that
changed.

`meta.redrawnWidgets` lists those whose picture the engine drew again, such as a text field's
widgets after a new value. A checkbox or radio button that switches shows another picture it
already has, so it isn't in the list.

### Checkboxes with several widgets

A checkbox can have several widgets, each with its own `exportValue`: a "Yes / No" pair, for
example. `{ checked: true }` checks the first widget; `{ value: 'No' }` checks the widget with
that export value.

In the file, each checkbox and radio widget also has an `onState`: the name of its checked look
(PDF's appearance state). It's usually the same as the export value, and widgets the engine
creates always use their `exportValue` for it. You never need it to fill a field in.

The field's value in the file (`valueEntry`) is the checked widget's `onState`, or `'Off'`, as the
PDF standard says. Some tools write the widget's position instead (`'0'`, `'1'`); such a value
still resets to that widget.

### XFA forms

`formKind` is `'xfa'` when the PDF also carries an XFA form, an older XML form format. The
engine works with the document's regular fields and never runs XFA.

### Fields stored inline

A field that the file stores inside another object, rather than as an object of its own, can be
read but not changed: writes fail with `InvalidArg`. PDF tools rarely write fields this way.

---

### Cloudpdf Website Src Content Docs Headless Annotations Index (`cloudpdf/website/src/content/docs/headless/annotations/index.mdx`)

---
title: Annotations
description: Show the annotations in a PDF, let people draw new ones, and work with them from code.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

import { Cards, Card } from '@/components/docs/cards';

{/* This page is served at `…/headless/<framework>/annotations` with NO trailing
    slash, so a relative href resolves against `…/headless/<framework>/`.
    Sibling pages are `annotations/tools`, other sections `text/selection`. */}

# Annotations

Annotations are what people add on top of a page: highlights, sticky notes, shapes, stamps and
text boxes. The annotation plugin shows the ones a PDF already has, lets people draw and edit new
ones, and gives your code the same abilities. Every annotation is saved into the PDF, so it looks
the same in every other PDF app.

## Show the annotations in a PDF

Register `interactionPlugin()` and `annotationPlugin()`, then put an `<AnnotationLayer>` above the
rendered page:

<Example name="annotation/layer" />

> **Try it:** click the rectangle and drag it, or a corner to resize it. Double-click the text box
> to type in it.

<Snippet name="annotations/index/layer" />

While the annotation layer is on the page, the render layer leaves the annotations out of the page
picture by itself. The annotation layer draws them instead, so they can be selected, moved and
changed.

`interactionPlugin()` is the shared input system. Every tool, from text selection to drawing,
speaks through it; see [Selecting & copying](text/selection) for how it works.

## Let people draw

A **tool** says what a click or a drag on the page creates. Activate one and people can draw:

<Example name="annotation/draw" />

> **Try it:** drag on the page to draw a rectangle, then pick another tool. **Highlight** marks the
> text you select.

<Snippet name="annotations/index/toolbar" />

- **`pointer`** selects: click an annotation to select it, then move, resize or turn it.
- **`square`** draws a rectangle: drag, or just click for a default-size one.
- **`ink`** draws freehand.
- **`highlight`** highlights the text you select.
- **`note`** drops a sticky note where you click.

After you draw, the new annotation is selected and the tool stays active, so you can draw another.
You can change both; see [What happens after drawing](annotations/tools#what-happens-after-drawing).

## How it fits together

Everything on these pages is one of five things:

| Thing          | What it is                                                                         | Where you use it                           |
| :------------- | :--------------------------------------------------------------------------------- | :----------------------------------------- |
| **Annotation** | A record in the PDF: its kind, position, colors and text.                          | `annotation.list()`                        |
| **Tool**       | What a click or drag creates, with which defaults, cursor and preview.             | `annotationPlugin({ tools })`              |
| **Layer**      | What each page draws: the annotations, the selection box and handles, the preview. | `<AnnotationLayer>`                        |
| **Overlay**    | Your own UI attached to an annotation, such as a menu or a hover card.             | `<AnnotationMenu>`                         |
| **API**        | Reading, creating, changing and deleting annotations, and listening for changes.   | `useAnnotation()`                          |

An annotation here is the same record the engine reads and writes: the same kinds, the same fields
and the same names. What you learn in the [engine's annotation pages](/docs/engine/annotations)
holds here too.

## The built-in tools

| Tool                                              | Makes                                       | How                                   |
| :------------------------------------------------ | :------------------------------------------ | :------------------------------------ |
| `square`, `circle`                                | A rectangle or an ellipse                   | Drag, or click for a default size     |
| `line`                                            | A line                                      | Drag, or click for a default length   |
| `polygon`, `polyline`                             | A closed or open shape of points            | Click each point, double-click to end |
| `ink`, `ink-highlight`                            | Freehand drawing, or a see-through marker   | Draw                                  |
| `free-text`, `free-text-callout`                  | A text box, or one with a line pointing out | Click or drag, then type              |
| `highlight`, `underline`, `strikeout`, `squiggly` | A mark over text                            | Select text                           |
| `insert-text`, `replace-text`                     | A suggested insertion or replacement        | Select text                           |
| `note`                                            | A sticky note                               | Click                                 |
| `stamp`                                           | An image or a PDF drawing                   | Click, then pick a file               |
| `attachment`                                      | A file pinned to a spot                     | Click, then pick a file               |
| `link`                                            | An area that opens a page or a website      | Drag                                  |

Measurement tools (`distance`, `perimeter`, `area`) and redaction (`redact`) are annotation tools
too. Their plugins add the rest of what they need.

You can change any tool, or add your own; see [Tools & defaults](annotations/tools).

## Who wrote it

Every annotation records who made it and who last changed it (`author`, `createdAt`,
`modifiedAt`). You don't set these on the annotation: they come from the user the document was
opened for.

<Engine local>

Tell the viewer who the user is, once. Every document it opens uses it:

<Example name="annotation/identity" />

> **Try it:** draw a rectangle. The list says Dana Smith made it, and when.

<Snippet name="annotations/index/identity" />

`displayName` becomes the `author` other PDF apps show, and `userId` records who owns the
annotation. The same identity decides which annotations count as the user's own when
[permissions](concepts/permissions) say "only their own".

To open one document as someone else, give its entry in `initialDocuments` its own `identity`. It
replaces the viewer's for that document:

```ts
const initialDocuments = [
  { source: contract, identity: { userId: 'bot', displayName: 'Import job' } },
];
```

A document keeps the identity it was opened with. <Fw only={['react', 'vue', 'svelte']}>If you change the viewer's `identity`, documents
you open afterwards use the new one.</Fw>

<Fw only="angular">

To change the viewer's identity, for example when someone else signs in, use
`inject(EpdfViewer).updateSettings({ identity })`. Documents you open afterwards use the new one. To
follow your app's login:

```ts
effect(() => viewer.updateSettings({ identity: auth.identity() }));
```

</Fw>

</Engine>

<Engine cloud>

It comes from the identity in the user's token; see
[Authentication](/docs/engine/authentication#what-a-token-carries).

</Engine>

## Settings

Set them when you register the plugin. `snap`, `chrome`, `afterCreate` and `editable` can change
while the app runs, with `annotation.updateSettings({ … })`; `annotation.resetSettings()` goes back to what you
registered, and `useAnnotationSettings()` gives you the current settings.

| Setting       | What it does                                                          | Page                                                        |
| :------------ | :-------------------------------------------------------------------- | :---------------------------------------------------------- |
| `tools`       | Change the built-in tools, or add your own                            | [Tools & defaults](annotations/tools)                       |
| `afterCreate` | Select a new annotation? Keep the tool active?                        | [What happens after drawing](annotations/tools#what-happens-after-drawing) |
| `chrome`      | The selection's outline, handles and rotation handle, and their colors | [Change the handles](annotations/selecting#change-the-handles) |
| `snap`        | Snapping to other annotations and to angles                           | [Snapping](annotations/selecting#snapping)                  |
| `editable`    | Which annotations people may change by hand: `true`, `false`, or kinds | [What people may change](annotations/selecting#what-people-may-change) |

Every color in `chrome` can also come from CSS, for dark mode or a different look per viewer: see
[Colors & theming](ui/theming).

## Next steps

<Cards>
  <Card
    title="Annotations in code"
    description="Read, create, change and delete annotations from your own code."
    href="annotations/with-code"
  />
  <Card
    title="Events, saving & sharing"
    description="Listen for changes, save them into the PDF, or move them to your own server."
    href="annotations/saving"
  />
  <Card
    title="Tools & defaults"
    description="Default colors, your own tools, the cursor, and a preview before placing."
    href="annotations/tools"
  />
  <Card
    title="Selecting & editing"
    description="Selection, a menu, a style panel, and your own handles."
    href="annotations/selecting"
  />
  <Card
    title="Your own annotation look"
    description="Draw an annotation with your own component, and show cards on hover."
    href="annotations/custom-rendering"
  />
  <Card
    title="Comments & replies"
    description="Build a comments sidebar with threads, replies and review states."
    href="annotations/comments"
  />
</Cards>

---

### Cloudpdf Website Src Content Docs Headless Forms Index (`cloudpdf/website/src/content/docs/headless/forms/index.mdx`)

---
title: Filling forms
description: Let people fill in PDF forms, read and set values from your code, check required fields, and move form data in and out.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

# Filling forms

Many PDFs are forms: text boxes, checkboxes, dropdowns, signature fields. Register `formPlugin()` and
put `<FormLayer>` in each page, and people can fill them in:

<Snippet name="forms/form-layer" />

<Example name="form/basic" />

Each field becomes a real HTML control, placed and styled like the field in the PDF: people type in text
boxes, tick checkboxes, pick from lists, and move between fields with Tab. A value is saved into the
document as it's entered, so it's there when the PDF is downloaded, and every PDF app shows it. The
form layer paints the fields as the engine draws them, so while it's on the page, the render layer
leaves them out of the page picture.

## Read the form

<Fw only={['react', 'vue', 'svelte']}>

`useFormState()` gives you every field, and <Word of="rerenders" /> when a value changes:

</Fw>

<Fw only="angular">

`form.fields()` is every field, as a signal: your view updates when a value changes.

</Fw>

<Snippet name="forms/field-list" />

Each field has its full `name` (such as `billing.name`), its `family` (`'text'`, `'checkbox'`,
`'radio'`, `'combobox'`, `'listbox'`, `'pushbutton'` or `'signature'`), its value, and a `ref`: its
address for everything else. The fields and their values are the engine's; see
[Forms](/docs/engine/forms#read-the-form) for every family's value.

For one field, such as a total you show in your own UI, use `useFormValue()`:

<Snippet name="forms/invoice-total" />

`toFieldRef(name)` makes a ref from a field's full name, for when you know a field by name.

<Example name="form/read" />

## Fill in a field

`setValue()` takes the same values a field reads:

```ts
await form.setValue(name.ref, { value: 'Ada Lovelace' }); // text
await form.setValue(agree.ref, { checked: true }); // checkbox
await form.setValue(plan.ref, { value: 'yearly' }); // radio group: the button's export value
await form.setValue(country.ref, { value: 'NL' }); // dropdown: an option's value
await form.setValue(toppings.ref, { selectedValues: ['cheese', 'olives'] }); // list
```

It runs the same checks and scripts as typing does, and resolves with `{ field, status }`: `status`
is `'applied'`, `'unchanged'`, or `'rejected'` when the form's own validation refused the value.

The field shows the new value at once, before the engine answers, and the promise resolves once it
has. What the form's scripts calculate from it goes with it, as one change: one request, and one
step to undo. The field stays editable meanwhile; filling it in again is the next change. A value
the engine refuses goes back to the one the field had.

<Fw only="angular">

Your own inputs can fill in fields too. `form.controlOf(ref)` gives you a field as an Angular
`FormControl`: it holds the field's value, and fills in the field when the control changes, with
the same checks and scripts:

<Snippet name="forms/form-control" />

</Fw>

To fill many fields at once, such as from your backend, pass them by name:

```ts
await form.importValues({ 'billing.name': 'Ada Lovelace', 'billing.country': 'NL', agree: true });
```

`importValues()` and `setValues()` write every value as one change, with the calculations run once
after them. Each field's own keystroke and validation scripts don't run, as when a script sets a
field's `value` in Acrobat.

<Example name="form/fill" />

## Check required fields

A form marks some fields as required. Check them before you submit or download:

```ts
const { valid, missing } = form.validate();

const widget = !valid && form.getWidget(missing[0].widgets[0]);
if (widget) stage.reveal(widget.page, { rect: widget.rect });
```

`missing` lists the required fields that are empty, in page order, so you can take the user to the
first one. Only the fields the user may fill in count: another signer's are theirs to fill.

<Example name="form/validate" />

## Calculations, formatting and validation

Many forms add up totals, format dates and check values with scripts. With JavaScript on in the
[actions plugin](documents/actions), they run as people type, as in Acrobat:

<Snippet name="forms/scripts" />

When a script refuses a value, such as a date in the wrong format, the field keeps its old value,
the box keeps what was typed so it can be corrected, as in Acrobat, and
<Fw only={['react', 'vue', 'svelte']}>`onValidationRejected`</Fw><Fw only="angular">`validationRejected$`</Fw> tells you why, so you can show a message.

The scripts read the form as the user sees it, values still on their way included, so nothing is
read from the engine for them. Their engine starts when the first field of a form with scripts takes
focus, so the first value doesn't wait for it; a form without scripts never starts it.

## Reset the form

```ts
await form.reset(); // every field the user may fill in, back to its default
await form.reset([name.ref, email.ref]); // some fields
```

## Move form data in and out

For your own backend, plain values are simplest:

```ts
const values = form.exportValues(); // { 'billing.name': 'Ada Lovelace', agree: true, … }
await form.importValues(values);
```

Fields are matched by their full name. A field the form doesn't have, or a value it can't take, is
skipped; the rest still land.

To copy the fields themselves to another document, with their boxes, values and scripts, take
them out as a bundle and import it there. `FormTransfer` turns a bundle into one JSON text to
store, and back:

```ts
const bundle = await form.export(); // every field; or form.export({ fields }), ({ pages })
const text = FormTransfer.stringify(bundle);

const { fields, dropped } = await otherForm.import(await FormTransfer.parse(text));
```

The import adds the fields as one change you can undo. A field whose name the form has already is
left out and listed in `dropped`. By default each field keeps who created and filled it in, which
needs `canRestoreAttribution()`; pass `{ attribution: 'stamp' }` to make them yours. See
[Import & export](/docs/engine/forms/import-export).

<Example name="form/data" />

## Methods

What `useForm()` gives you for reading and filling. The verbs that change the form itself are
on [Building forms](forms/building#methods).

| Method                     | What it does                                                                                                                                   |
| :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- |
| `get(ref)`                 | One field, with its value, or `null` when the form has no such field. [Read the form](#read-the-form)                                           |
| `getValue(ref)`            | One field's value, in the shape `setValue()` takes, or `null`. In a component, use `useFormValue(ref)`.                                        |
| `getFieldForWidget(widget)` | The field a box belongs to, from the box's ref                                                                                                |
| `getWidget(widget)`        | A box's row: its page, its place (`rect`), its look and its state, or `null` when no page shows it                                             |
| `getWidgets(ref)`          | Every box a field shows in, as rows in the field's order: a radio group's buttons, or a field repeated on several pages                       |
| `listWidgets(page)`        | Each field box on a page, with its place, its control, its value and whether it can be filled. For drawing the fields yourself.                |
| `getWidgetAt(page, point)` | The field box under a point on a page, with its field, or `null`                                                                               |
| `setValue(ref, value)`     | Fills in a field with `{ value }`, `{ checked }` or `{ selectedValues }`, running the form's checks and scripts. Resolves `{ field, status }`. [Fill in a field](#fill-in-a-field) |
| `setValues(entries)`       | Fills in several fields, each `{ ref, value }`, as one change with one recalculation; a field you may not fill in, or a value it can't take, fails on its own. Resolves `{ applied, skipped, failed }`. |
| `importValues(values)`     | Fills in fields by full name from a plain object, as one change with one recalculation. Resolves `{ applied, skipped, failed }`. [Move form data in and out](#move-form-data-in-and-out) |
| `exportValues()`           | Every value, keyed by full name, as a plain object                                                                                             |
| `export(selection?)`       | Whole fields with their boxes, values and scripts, as a bundle: every field, or `{ fields }` or `{ pages }`. [Move form data in and out](#move-form-data-in-and-out) |
| `import(bundle, options?)` | Adds a bundle's fields, as one change. Resolves `{ fields, refMap, dropped }`. Needs `canDesign()`. [Move form data in and out](#move-form-data-in-and-out) |
| `reset(refs?)`             | Puts back each field's default, for every field the user may fill in or the fields you pass. Resolves `{ fields }`, the fields it changed. [Reset the form](#reset-the-form) |
| `validate()`               | Checks the required fields the user may fill in. Returns `{ valid, missing }`, the empty ones in page order. [Check required fields](#check-required-fields) |
| `activateWidget(widget)`   | Does what a click on a field box does, such as running a button's action                                                                       |
| `getSnapshot()`            | Everything at once, as the user sees it: what the engine's `list()` returns, with the values still on their way. `fields`, `widgets`, `formKind`, `needsAppearances` and `calculationOrder` |
| `refresh()`                | Reads the whole form again, and <Fw only={['react', 'vue', 'svelte']}>`onResynced`</Fw><Fw only="angular">`resynced$`</Fw> fires. You rarely need it: the plugin follows every change, whoever made it.                      |
| `canRead()`, `canFill(ref)` | Whether reading the form, and filling in a field, are allowed. [Permissions](#permissions)                                                   |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()` | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use `useFormSettings()`. [Settings](#settings) |

Every method that returns a promise also takes a `signal` in its options, to cancel it.

## State

<StateIntro hook="useFormState()" />

| Field           | What it is                                                                                      | Read once                                            |
| :-------------- | :---------------------------------------------------------------------------------------------- | :--------------------------------------------------- |
| `fields`        | Every field, with its value                                                                     | `list()`, or `list({ family, page, name })` for some |
| `status`        | Whether the form is read: `'idle'`, `'loading'`, `'ready'`, `'forbidden'` or `'error'`           | `getStatus()`                                        |
| `formKind`      | What the document has: `'none'`, `'acroform'` (a regular PDF form) or `'xfa'`                   | `getFormKind()`                                      |
| `selectedField` | In design mode, the field of the selected box, or `null`; see [Building forms](forms/building#change-a-fields-settings) | `getSelectedField()`          |

For one field, `useFormValue(ref)` <Word of="rerenders" /> only when that field's value changes.

## Settings

Set them when you register the plugin, and change them while the app runs with
`form.updateSettings({ … })`; `form.resetSettings()` goes back to what you registered, and
`useFormSettings()` gives you the current settings.

| Setting             | What it does                                                               | Default                      | From CSS                       |
| :------------------ | :------------------------------------------------------------------------- | :--------------------------- | :----------------------------- |
| `validation`        | Run the form's own checks (`'scripts'`), or accept every value (`'none'`)  | `'scripts'`                  | —                              |
| `focus.color`       | The ring around the field being filled in                                  | the viewer's accent          | `--epdf-form-focus`            |
| `fields.border`     | The edge of a field that has no border of its own                          | the viewer's accent at 55%   | `--epdf-form-field-border`     |
| `fields.background` | Inside a field that has no background of its own                           | `'rgb(255 255 255 / 0.92)'`    | `--epdf-form-field-background` |
| `fields.color`      | The check mark and text of a field that has no color of its own            | `'#1f2a44'`                    | `--epdf-form-field-color`      |
| `fillable`          | Which fields people fill in by hand: `true`, `false`, or field families    | `true`                       | —                              |

Scripts need JavaScript on in the actions plugin; without it, `'scripts'` accepts every value too,
and a form with scripts logs a hint once.

`fillable` is for the people using your app. A field it leaves out draws as a read-only field
does, and takes no typing, clicks or picks; your code still fills it in with `setValue()` and
`importValues()`, and `canFill(ref)` doesn't change:

```ts
formPlugin({ fillable: false }); // show the values, take no input
form.updateSettings({ fillable: ['text', 'checkbox'] }); // only these families
form.updateSettings({ fillable: true }); // every field again
```

The families are `'text'`, `'checkbox'`, `'radio'`, `'combobox'`, `'listbox'`, `'pushbutton'` and
`'signature'` (signing a field is filling it in). To stop your code too, leave `doc.forms.fill` out
of the scope (see [Permissions](#permissions)).

The colors are what the viewer draws around fields; a field's own colors are saved in the PDF (see
[Building forms](forms/building)). A CSS variable wins over the setting: see
[Colors & theming](ui/theming).

<Example name="form/colors" />

## Events

<Snippet name="forms/autosave" />

| Event                  | Fires when                                                                | Carries           |
| :--------------------- | :------------------------------------------------------------------------ | :---------------- |
| `onValueChanged`       | A field's value changed                                                   | `field`, `origin` |
| `onValidationRejected` | The form's validation refused a value                                     | `field`, `issues` |
| `onResynced`           | The whole form was read, as when it opens, after `refresh()` or a repair  | `snapshot`        |
| `onSettingsChanged` | The settings changed | `settings`, `changed` |

Fields that are added, changed or removed have their own events; see
[Building forms](forms/building#events).

## Permissions

| Permission       | What it allows   | Check               |
| :--------------- | :--------------- | :------------------ |
| `doc.forms.read` | Reading the form | `form.canRead()`    |
| `doc.forms.fill` | Filling it in    | `form.canFill(ref)` |

Without it, the fields show their values but can't be changed. A user with a `fields:fill`
permission for one group fills in that group's fields only: `form.canFill(ref)` answers per field,
the other fields can't be changed and Tab skips them, `validate()` leaves them out, and `reset()`
leaves them as they are. See [Signers and groups](/docs/engine/forms/groups).

## Good to know

### Forms and annotations

A field's widgets are the form's: the annotation plugin never lists them. With the
[annotation plugin](annotations) registered, the `<FormLayer>` and the annotation layer share the page: fields stay fillable with the
`pointer` tool, and become movable boxes in [design mode](forms/building).

### Keyboard

Tab moves to the next field in the page's order. In a text box, Enter confirms and Escape puts back
the value it had.

### Collecting responses

<Engine local>

On the local engine, the values live in the document in this tab until it's downloaded.

<CloudPdfCallout
  title="Collect form responses on your server"
  href="https://www.cloudpdf.com/docs/headless/forms"
  cta="Collect forms with CloudPDF"
>

With CloudPDF, each value is stored on your server as it's entered, so a half-filled form survives a
closed tab, and your backend reads the answers without asking the user to download or upload a
file.

</CloudPdfCallout>

</Engine>

<Engine cloud>

Each value is stored on your server as it's entered, so a half-filled form survives a closed tab.

</Engine>

### XFA forms

Some PDFs also carry a form in the older XFA format. The viewer works with the document's regular
fields and never runs XFA; see [Forms](/docs/engine/forms#xfa-forms).

### Signed documents

A signature can lock the whole form, or some fields. Locked fields show their values and can't be
changed; see [Signatures](forms/signatures).

---

### Cloudpdf Website Src Content Docs Server Configuration Index (`cloudpdf/website/src/content/docs/server/configuration/index.mdx`)

---
title: Configuration reference
description: Every CLOUDPDF_* environment variable the CloudPDF server reads, with defaults and where each one matters.
---

import { Callout } from '@/components/docs/callout';
import { Cards, Card } from '@/components/docs/cards';

# Configuration reference

The server is configured entirely through environment variables. The same
variables work whether you run it via [npm](/docs/server/deployment/npm),
[Docker](/docs/server/deployment/docker),
[Compose](/docs/server/deployment/docker-compose), or
[Helm](/docs/server/deployment/helm) — only *how* you set them differs.

<Callout type="info">
  Only one variable is strictly required:
  <strong><code>CLOUDPDF_LICENSE_KEY</code></strong> (see
  <a href="/docs/server/configuration/licensing">Licensing</a>). With a
  development key, everything else has a sensible default — a minimal server
  runs with just that. Any other license kind also requires
  <code>CLOUDPDF_JWT_SECRET</code> and
  <code>CLOUDPDF_PASSWORD_VERIFICATION_HMAC_SECRET</code> (plus
  <code>CLOUDPDF_PASSWORD_SESSION_SERVER_SECRET</code> when a KMS is
  configured) to be real random values of at least 32 bytes — the server fails
  closed at boot and names the variable it needs.
</Callout>

## Licensing

| Variable | Default | Description |
| --- | --- | --- |
| `CLOUDPDF_LICENSE_KEY` | — | **Required.** Your license key; `secret://` references resolve through the secrets providers. |
| `CLOUDPDF_LICENSE_MODE` | `connected` when a key is set | `connected` or `air-gapped`. |

See [Licensing](/docs/server/configuration/licensing).

## Process

| Variable | Default | Description |
| --- | --- | --- |
| `PORT` | `3000` | Port the HTTP server listens on. |
| `HOST` | `0.0.0.0` | Interface to bind. |
| `CLOUDPDF_WORKER_POOL_SIZE` | `min(2, cpus)` | Native PDFium worker threads. Integer, or `max` to use all cores. See [Health & scaling](/docs/server/operations/health-and-scaling). |

## Engine plane

| Variable | Default | Description |
| --- | --- | --- |
| `CLOUDPDF_ENGINE_ISOLATION` | `inline` | `host` runs PDFium in a supervised child process: a native crash costs a sub-second engine respawn instead of the instance. See [Health & scaling](/docs/server/operations/health-and-scaling). |
| `CLOUDPDF_ENGINE_SHARDS` | `1` | Host mode: split the engine into K child processes (worker total must divide evenly). One crash costs 1/K of open documents. |
| `CLOUDPDF_ENGINE_MAX_IN_FLIGHT` | workers × 2 | Admission cap on concurrently dispatched engine jobs; beyond the bounded queue the server sheds with `503` + `Retry-After`. |
| `CLOUDPDF_ENGINE_BG_MAX_IN_FLIGHT` | ⌊workers / 2⌋ | Of the cap, how much background work (thumbnail warming) may occupy. `0` disables the background lane entirely. |
| `CLOUDPDF_ENGINE_RECYCLE` | off | `1` opts into memory-watermark engine recycling (host mode). Companion knobs: `_RECYCLE_SOFT_PCT` (70), `_RECYCLE_HARD_PCT` (85), `CLOUDPDF_ENGINE_MAX_RSS_MB`, `CLOUDPDF_ENGINE_MAX_LIFETIME_HOURS`. |
| `CLOUDPDF_ENCODE_IN_ENGINE` | on | `0` reverts image encoding to the API process (one-release escape hatch; rasters cross the engine boundary raw). |

## Authentication

| Variable | Default | Description |
| --- | --- | --- |
| `CLOUDPDF_JWT_SECRET` | dev fallback | HS256 secret used to verify document and admin tokens. Must match the secret your backend signs with. **Required (≥32 bytes) under a production license**; development keys fall back to a public dev value with a warning. |
| `CLOUDPDF_API_AUTH_TOKENS` | — | Comma-separated static API tokens for trusted backends and tooling (root credential for the admin plane). ≥32 bytes each under a production license. |
| `CLOUDPDF_PASSWORD_VERIFICATION_HMAC_SECRET` | dev fallback | HMAC pepper for the encrypted-PDF password-verification cache — it keeps stored password proofs non-reversible. **Required (≥32 bytes) under a production license**, whether or not you open encrypted PDFs. |
| `CLOUDPDF_PASSWORD_SESSION_SERVER_SECRET` | dev fallback | Server key material for encrypted-PDF unlock sessions. **Required (≥32 bytes) under a production license when a KMS is configured**; unused otherwise. |
| `CLOUDPDF_PASSWORD_SESSION_SERVER_SECRET_ID` | `dev-v1` | Label for the active session secret, recorded on each session. Changing it (or the secret) invalidates existing sessions. |

See [Authentication](/docs/server/configuration/authentication), and
[Encrypted-PDF secrets](/docs/server/configuration/authentication#encrypted-pdf-secrets)
for what the last three protect.

## Database

| Variable | Default | Description |
| --- | --- | --- |
| `CLOUDPDF_DB_DRIVER` | `sqlite` | `sqlite` or `postgres`. |
| `CLOUDPDF_DB_SQLITE_PATH` | `./data/cloudpdf.db` | SQLite file path (when driver is `sqlite`). |
| `CLOUDPDF_DB_URL` | — | Postgres connection string (required when driver is `postgres`). |

See [Database](/docs/server/configuration/database).

## Storage (object store)

| Variable | Default | Description |
| --- | --- | --- |
| `CLOUDPDF_STORAGE_KIND` | `fs` | `fs`, `s3`, `gcs`, or `azure-blob`. |
| `CLOUDPDF_STORAGE_FS_ROOT` | `./data/objects` | Directory for stored objects (when kind is `fs`). |
| `CLOUDPDF_STORAGE_S3_BUCKET` | — | Bucket name (S3). |
| `CLOUDPDF_STORAGE_S3_REGION` | — | Bucket region (S3). |
| `CLOUDPDF_STORAGE_S3_ENDPOINT` | — | Custom endpoint for S3-compatible stores (e.g. MinIO). |

See [Storage](/docs/server/configuration/storage).

## Imports (server-side pull)

On by default with a safe posture (HTTPS-only, public networks, 128 MiB cap).
Registered connections let the server pull from your own buckets without
presigning.

| Variable | Default | Description |
| --- | --- | --- |
| `CLOUDPDF_IMPORT_ENABLED` | on | Set `0` to disable the import endpoint. |
| `CLOUDPDF_IMPORT_MAX_BYTES` | `128 MiB` | Per-document transfer ceiling. |
| `CLOUDPDF_IMPORT_TIMEOUT_MS` | `120000` | Wall-clock budget per transfer attempt. |
| `CLOUDPDF_IMPORT_MAX_CONCURRENT` | `4` | Concurrent transfers per replica (sync + async). |
| `CLOUDPDF_IMPORT_ALLOW_HTTP` | off | Permit `http://` URL sources (dev / MinIO). |
| `CLOUDPDF_IMPORT_ALLOW_PRIVATE_NETWORKS` | off | Permit private-address URL sources (dev / in-VPC). |
| `CLOUDPDF_IMPORT_CONNECTIONS` | — | Comma-separated registry of named pull sources (S3, GCS, Azure Blob, filesystem), each configured via `CLOUDPDF_IMPORT_CONNECTION_*` variables. |

See [Imports](/docs/server/configuration/imports).

## Engine cache

The cache holds recently used documents and renders so repeat requests are fast.
It is disposable — the server rebuilds it from the object store.

| Variable | Default | Description |
| --- | --- | --- |
| `CLOUDPDF_CACHE_ROOT` | `./data/cache` | Cache directory. |
| `CLOUDPDF_CACHE_MAX_BYTES` | `4 GiB` | Maximum cache size before eviction. |

## Fonts

| Variable | Default | Description |
| --- | --- | --- |
| `CLOUDPDF_FALLBACK_FONTS` | — | JSON array of fallback fonts (`{ key, path, … }`) for glyphs a document's own fonts are missing. |

See [Fallback fonts](/docs/server/configuration/fonts).

## Lifecycle & migrations

| Variable | Default | Description |
| --- | --- | --- |
| `CLOUDPDF_AUTO_MIGRATE` | on for SQLite, off for Postgres | Apply pending migrations on boot. |
| `CLOUDPDF_FAIL_ON_PENDING` | off | Refuse to start if migrations are pending. Recommended for multi-replica deploys. |
| `CLOUDPDF_AUTO_PROVISION_TENANT` | off | Lazily create tenant rows. Dev convenience — leave off in production. |

See [Migrations](/docs/server/operations/migrations).

## Shutdown & observability

| Variable | Default | Description |
| --- | --- | --- |
| `CLOUDPDF_SHUTDOWN_TIMEOUT_MS` | `30000` | Budget for closing in-flight HTTP during shutdown before teardown proceeds anyway. Keep it below your orchestrator's kill deadline. |
| `CLOUDPDF_SHUTDOWN_DRAIN_MS` | `0` | Settle window after `/readyz` flips to 503 (and SSE streams end) before the listener closes — for probe-driven load balancers. |
| `CLOUDPDF_METRICS` | off | `1` exposes an unauthenticated Prometheus `/metrics` endpoint (scrape it inside your private network). |

## Optional adapters

These are opt-in. You only need them for the corresponding feature.

| Variable | Default | Description |
| --- | --- | --- |
| `CLOUDPDF_CDN_KIND` | `none` | Sign delivery URLs via a CDN: `bunny`, `cloud-cdn`, `cloudfront`, `azure-fd`, `custom-hmac`. |
| `CLOUDPDF_KMS_KIND` | `static` | Key management for encrypted-PDF sessions: `aws-kms`, `gcp-kms`, `azure-kv`. |
| `CLOUDPDF_SECRETS_PROVIDERS` | `env` | Comma-separated registry of secret sources. |

See [CDN, KMS & secrets](/docs/server/configuration/adapters).

<Callout type="info">
  The CLI prints this whole surface at any time:
  <code>cloudpdf-server --help</code>.
</Callout>

## Where to set these

<Cards>
  <Card
    title="npm"
    description="Shell environment or a process-manager unit file."
    href="/docs/server/deployment/npm"
  />
  <Card
    title="Docker"
    description="-e flags, or an --env-file."
    href="/docs/server/deployment/docker"
  />
  <Card
    title="Docker Compose"
    description="The environment: block and a .env file."
    href="/docs/server/deployment/docker-compose"
  />
  <Card
    title="Helm"
    description="config: (ConfigMap) and existingSecret (Secret)."
    href="/docs/server/deployment/helm"
  />
</Cards>

---

### Cloudpdf Website Src Content Docs Server Deployment Index (`cloudpdf/website/src/content/docs/server/deployment/index.mdx`)

---
title: Deployment overview
description: Choose how to run the CloudPDF server — npm, Docker, Docker Compose, or Helm — based on your scale and operational needs.
---

import { Cards, Card } from '@/components/docs/cards';
import { Callout } from '@/components/docs/callout';

# Choosing a deployment

The server is one application, packaged four ways. They run the same binary and
read the same [`CLOUDPDF_*` configuration](/docs/server/configuration), so the
choice is purely about *how you want to operate it* — not what it can do.

| Method | Best for | Typical database | Typical storage | Scaling |
| --- | --- | --- | --- | --- |
| [**Node / npm**](/docs/server/deployment/npm) | Local dev, existing Node platforms, custom hosts | SQLite or Postgres | Disk or cloud | Manual |
| [**Docker**](/docs/server/deployment/docker) | Single host, simple self-hosting, CI | SQLite or Postgres | Disk or cloud | Manual / your orchestrator |
| [**Docker Compose**](/docs/server/deployment/docker-compose) | A full self-hosted stack on one box | Postgres (included) | MinIO / S3 (included) | A few replicas, one host |
| [**Helm / Kubernetes**](/docs/server/deployment/helm) | Production, startup → enterprise | Postgres (managed) | S3 / GCS / Azure (managed) | Autoscaling, multi-replica, HA |

<Callout type="info">
  You can always start small and grow. Because storage is configured, not
  compiled in, moving from SQLite + disk to Postgres + S3 — or from Docker to
  Kubernetes — is a configuration change, not a rewrite.
</Callout>

## A quick decision guide

- **Just exploring, or developing locally?** Use [npm](/docs/server/deployment/npm)
  or the [Quick start](/docs/server/getting-started/quick-start) Docker one-liner.
- **Self-hosting on a single server or VM?** Use
  [Docker](/docs/server/deployment/docker) for the server alone, or
  [Docker Compose](/docs/server/deployment/docker-compose) for the server plus
  Postgres and object storage in one file.
- **Running in production, or need high availability?** Use
  [Helm](/docs/server/deployment/helm). You get multiple replicas, autoscaling,
  managed schema migrations, and a clean separation of secrets — the things that
  matter when downtime is expensive.

## What every method needs

No matter how you deploy, the server needs:

1. **A signing secret** — `CLOUDPDF_JWT_SECRET`, shared with the backend that
   mints tokens. See [Authentication](/docs/server/configuration/authentication).
2. **A database** — SQLite (default) or Postgres. See
   [Database](/docs/server/configuration/database).
3. **An object store** — local disk (default) or a cloud bucket. See
   [Storage](/docs/server/configuration/storage).
4. **Persistent volumes** for `/data` when you use SQLite, local storage, or the
   local cache.

<Callout type="info">
  For anything customer-facing: set
  <code>CLOUDPDF_ENGINE_ISOLATION=host</code> so a native PDF crash costs a
  sub-second engine respawn instead of the instance, and run <strong>more
  than one replica</strong> (Compose or Helm) so a single failure can't take
  you offline. Details in
  <a href="/docs/server/operations/health-and-scaling">Health &amp; scaling</a>.
</Callout>

## Pick your path

<Cards>
  <Card
    title="Node / npm"
    description="Install the package, run the CLI."
    href="/docs/server/deployment/npm"
  />
  <Card
    title="Docker"
    description="One self-contained image."
    href="/docs/server/deployment/docker"
  />
  <Card
    title="Docker Compose"
    description="Server + Postgres + object storage."
    href="/docs/server/deployment/docker-compose"
  />
  <Card
    title="Helm / Kubernetes"
    description="Production-grade, autoscaling."
    href="/docs/server/deployment/helm"
  />
</Cards>

---

### Cloudpdf Website Src Content Docs Server Getting Started Index (`cloudpdf/website/src/content/docs/server/getting-started/index.mdx`)

---
title: Introduction
description: The CloudPDF server — a self-hostable PDF engine that renders pages, extracts text, manages annotations, and serves documents over a secure HTTP API.
---

import { Cards, Card } from '@/components/docs/cards';
import { Callout } from '@/components/docs/callout';

# The server

`@cloudpdf/server` is the backend that powers CloudPDF. It runs native PDFium on
your own infrastructure and exposes it as a clean, document-scoped HTTP API:
open a document, render pages, extract text, read and write annotations, inspect
metadata, and enforce access — all behind short-lived JWTs.

It is the same engine that runs locally in the browser with EmbedPDF, lifted
onto the server. Your frontend keeps talking to one [`Engine`
interface](/docs/engine); the server does the heavy PDF work, holds your
documents, and never hands storage credentials to the client.

<Callout type="info">
  In a hurry? The <a href="/docs/server/getting-started/quick-start">Quick start</a>
  gets a server answering requests in about a minute with a single
  <code>docker run</code>.
</Callout>

## Why run your own server

- **Your data stays yours.** Documents live in your database and object store —
  SQLite and the local disk to start, Postgres and S3/GCS/Azure when you grow.
- **Real PDF rendering.** Native PDFium renders pages to PNG/WebP and extracts
  text and geometry — the same fidelity as the desktop, on the server.
- **Secure by construction.** Every document route requires a short-lived,
  document-scoped JWT minted by your backend. The browser never sees your
  tenant secret or storage keys.
- **Runs anywhere.** A single Node process, a Docker container, or a
  Kubernetes deployment that scales from a side project to enterprise traffic.

## Three ways to run it

The server is a single, self-contained application. Pick the packaging that fits
your team — they all run the exact same binary and read the exact same
[configuration](/docs/server/configuration).

<Cards>
  <Card
    title="Node / npm"
    description="Install the package and run the CLI. Best for local dev and existing Node platforms."
    href="/docs/server/deployment/npm"
  />
  <Card
    title="Docker"
    description="One self-contained image. Best for single-host self-hosting and CI."
    href="/docs/server/deployment/docker"
  />
  <Card
    title="Docker Compose"
    description="The server plus Postgres and S3-compatible storage, on one host."
    href="/docs/server/deployment/docker-compose"
  />
  <Card
    title="Helm / Kubernetes"
    description="Production-grade: replicas, autoscaling, and managed migrations."
    href="/docs/server/deployment/helm"
  />
</Cards>

<Callout type="info">
  Don't want to host anything? <strong>CloudPDF Cloud</strong> runs this same
  server for you as a managed SaaS. The API is identical, so you can start on the
  cloud and move to self-hosted later — or the other way around — without
  changing a line of frontend code. [See plans and start a free trial](/pricing).
</Callout>

## Next steps

<Cards>
  <Card
    title="How it works"
    description="The request flow, the worker pool, and the storage model."
    href="/docs/server/getting-started/how-it-works"
  />
  <Card
    title="Quick start"
    description="Run a server and make your first authenticated request."
    href="/docs/server/getting-started/quick-start"
  />
  <Card
    title="Configuration reference"
    description="Every CLOUDPDF_* setting, with defaults."
    href="/docs/server/configuration"
  />
  <Card
    title="The engine client"
    description="Drive the server from your frontend with @cloudpdf/engine."
    href="/docs/engine"
  />
</Cards>

---

### Cloudpdf Website Src Content Docs Viewer Features Index (`cloudpdf/website/src/content/docs/viewer/features/index.mdx`)

---
title: All features
description: Everything the viewer can do, how to leave a feature out, who the user is, and where each feature's settings go.
---

{/* Generated from docs/content — edit there, then `pnpm docs:sync`. */}

# All features

Every feature is on from the start. Leave out the ones you don't need with `disable`, by the name
in the first column:

```js
{ disable: ['forms', 'redaction', 'measurement'] }
```

| Feature | What people can do | Settings |
| :-- | :-- | :-- |
| `annotations` | Highlight, underline and strike out text; draw; add notes, text boxes and callouts | [Annotations](features/annotations) |
| `shapes` | Draw rectangles, circles, lines, arrows and polygons | [Annotations](features/annotations#the-tools) |
| `comments` | Comment on annotations and reply, in a panel with every thread | [Annotations](features/annotations#comments) |
| `stamps` | Place stamps: the standard ones, their own, or images | [Annotations](features/annotations#stamps) |
| `signatures` | Draw, type or upload a signature; sign fields; check signatures | [Forms & signatures](features/forms-and-signatures#signatures) |
| `forms` | Fill in forms; build them in the Form mode | [Forms & signatures](features/forms-and-signatures) |
| `redaction` | Mark text and areas, then remove them from the file for good | [Annotations](features/annotations#redaction) |
| `measurement` | Measure distances, perimeters and areas at the drawing's scale | [Annotations](features/annotations#measurements) |
| `pages` | Rotate, move, insert, duplicate and delete pages in the thumbnails panel, each step undoable | [Panels](customize/panels#organize-pages) |
| `attachments` | See, save, attach and remove files inside the PDF | [Panels](customize/panels#the-attachments) |
| `search` | Find text, with every match highlighted and listed | [`plugins.search`](/docs/headless/text/search#settings) |
| `text` | Select and copy text | [`plugins.selection`](/docs/headless/text/selection#settings) |
| `thumbnails` | A picture of every page, to jump around | [Panels](customize/panels) |
| `outline` | The document's table of contents | [Panels](customize/panels) |
| `documents` | Open other files (with the local engine), download, print, several documents in tabs | [Documents](documents) |

## Leave features out

A feature in `disable` is gone everywhere at once: its buttons, its mode tab, its panel, its menu
rows and its shortcuts. Annotations already in the document still show; people just can't add new
ones. Most features have parts you can turn off on their own, such as `forms.building` or
`documents.download`, and one button goes by its command's name, such as `tool:highlight`.

`disable` follows its prop, so a feature can come and go while the viewer runs, for example when
a user's plan changes.

[Turn features off](features/turning-off) has every name, read-only, and what to use when: the
layout for where something goes, `disable` for whether the viewer has it, and the scope for what a
user may do.

## Who the user is

<Engine local>

The user's name becomes the author of the annotations and comments they make:

```js
{ identity: { displayName: 'Ada Lovelace' } }
```

</Engine>

<Engine cloud>

The user's name becomes the author of the annotations and comments they make. On CloudPDF, who the
user is and what they may do come from the document's token, so your server decides.

</Engine>

## Each feature's settings

Every feature is a headless plugin, and `plugins` takes its settings by the plugin's name, exactly
as the plugin's page lists them:

```js
{
  plugins: {
    stage: { spread: 'odd', zoom: { mode: 'fit-width' } },
    search: { highlight: { color: '#a7f3d0' } },
    annotation: { tools: [{ id: 'highlight', defaults: { color: '#fde047' } }] },
  },
}
```

That opens documents as two-page spreads at the page's width, with green search matches and
yellow highlights. See [Plugins](code/plugins) for every plugin and how to change its settings
while the viewer runs.

## Good to know

### What loads when

A feature you disable is still in the viewer's code: `disable` changes what people see, not what
you ship. The first load stays small another way. Each feature's panels and dialogs are files of
their own, which the viewer fetches in the background once the document shows, so they open at
once. The heavier parts wait until they're used: the signature checks until a signature is
checked, the standard stamps until Stamps opens, the handwriting fonts until someone types a
signature, and each language until it's chosen. [How it works](concepts/how-it-works#what-loads-when)
has the whole order.

---

### Conventions Architecture (`docs/conventions/architecture.md`)

# Architecture

How the client stack is put together: what a plugin is, where its state lives,
how engine data reaches the UI, and how a plugin's files are laid out. The
details live in [`state-and-sync.md`](./state-and-sync.md),
[`events.md`](./events.md) and [`plugins.md`](./plugins.md).

## A plugin in one paragraph

A plugin provides one capability per scope: one for the workspace, or one for
each open document. The kernel builds it by calling the plugin's
`create(ctx)`. The capability is the plugin's only public surface: synchronous
reads, verbs that do work, and events. Inside, a plugin holds up to four kinds
of state, each with one home and one way to change. It talks to the engine
only through `ctx.doc`, and to other plugins only through their capabilities.

## Layers

| Layer      | Packages                                                                                       | Responsibility                                                              |
| ---------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Engine     | `@embedpdf/engine-core` (the contract), `@embedpdf/engine` (local), `@cloudpdf/engine` (cloud) | Reads and writes PDF documents; publishes one confirmed event per mutation. |
| Kernel     | `@embedpdf/core`                                                                               | Scopes, plugin lifetime, the store, capability resolution, mirrors.         |
| Pure cores | `@embedpdf/core-*`                                                                             | One domain's logic, testable without a kernel, engine or DOM.               |
| Plugins    | `@embedpdf/plugin-*`                                                                           | One capability each, built on the kernel.                                   |
| Adapters   | `@embedpdf/react`, `@embedpdf/angular`, `@embedpdf/web`                                        | Render capabilities in one framework; read only through capabilities.       |
| Viewer     | `@embedpdf/viewer`, `@embedpdf/viewer-*`                                                       | The finished viewer, built on the adapters.                                 |

Where each package lives and what it is called: [`packages.md`](./packages.md).

An adapter paints from what a plugin hands out, and decides no geometry. When a
picture needs a placement, the plugin's read carries it resolved: an
annotation render item comes with its `frame` and `raster`, and the adapter
only converts page units to pixels with `@embedpdf/web` (`frameInPixels`,
`rasterInPixels`). The one core function adapters call is the annotation
core's `scene()`, the drawing vocabulary every painter maps onto its own
elements.

## Scopes and bring-up

A plugin declares `scope: 'workspace'` (the default: one instance for the
kernel) or `scope: 'document'` (one instance per open document).

- **Workspace plugins** are created by `createKernel`, in dependency order.
  Their `connect` runs in `kernel.start()`.
- **Document plugins** are created when a document opens. After the engine has
  opened the document and listed its pages, the kernel gives every document
  plugin a fresh state cell, then, in dependency order, runs each plugin's
  `create`, then its `connect`, then starts the first load of the mirrors that
  plugin created.

A document open is transactional. Everything above runs before the document is
published; if a `create` or `connect` throws, every teardown registered so far
runs, the document never becomes ready, and its tab shows `error`. Only after
bring-up does the document appear in `documents.list()` as `ready` and
`documents.onOpened` fire.

Closing a document removes its tab at once, revokes the write access of every
instance's state and mirror cells, aborts the instance lifetime (pending
`ctx.doc` calls reject with `instance-closed`), runs every registered teardown
in reverse order, and then fires `documents.onClosed`.

## Four kinds of state

| Kind           | What it is                                                                                  | Where it lives                                                                                  | How it changes                                                                                                                   | Examples                                                                                      |
| -------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| Mirror         | A local copy of data the engine owns                                                        | `ctx.mirror(spec)` or `ctx.pageMirror(spec)`: a store cell of its own                           | Only by folding confirmed engine events (every origin) and by loads. Verbs never write it.                                       | form fields, annotation records, metadata, signatures, links per page, text geometry per page |
| Pending change | A change this session staged that the engine hasn't answered                                | The document's change queue (`ctx.changes`), shared by its plugins; mirrors show it in `view()` | Staged by a verb or gesture; leaves the views once the engine answered and each mirror holds the answer, or at once when refused | three annotations the user moved, shown at their new places until the engine answers          |
| Session        | State the client owns that is not in the PDF                                                | `ctx.state`                                                                                     | Only through named pure transitions in `model.ts`                                                                                | camera, open surfaces, active tool, selection, search session, in-flight flags                |
| Resource       | Anything that is not plain data: handles, rasters, workers, registries of functions, caches | Closures in the controller, released through `ctx.cleanup` or `ctx.acquire`                     | However the owner likes; readers are woken with `ctx.notify()`                                                                   | raster store, tile manager, command and tool registries, script sandboxes                     |

What the UI sees is each mirror's `view()` (the confirmed value with the
pending changes replayed on top) and session state, combined by pure,
memoized reads.

Rules:

- A mirror always holds what the engine confirmed. Nothing optimistic enters
  it.
- A pending change leaves the views when the engine answered: an applied one
  once each mirror holds the answer, a refused one at once. Rollback is
  leaving: the views show the mirror again, never a copy taken before the
  change.
- A new object is named at birth: its create takes a reserved object number
  (`ctx.changes.takeObjectNumber()`), so everything that holds it before the
  engine answers holds its final ref.
- State is plain data: no functions, class instances, handles, `Map`s or
  `Set`s in `ctx.state` or a mirror. Anything else is a resource.
- No state exists only to wake the UI. A counter bumped so that selectors
  re-run is a `ctx.notify()` call instead.

[`state-and-sync.md`](./state-and-sync.md) describes each kind's API.

## Data flow

```
 user gesture or API call
          │
          ▼
   capability verb ──► ctx.changes.stage(change) ──► every mirror's view() shows it
          │
          ▼
   doc.apply(change), after everything staged before it
          │                                   another session's mutation
          ▼                                              │
   engine confirms ─► document event (origin: local) ◄───┘ (origin: remote)
          │
          ▼
   mirror fold(value, event)  ── the same code for every origin
          │
          ├─► new mirror value (its store cell)
          ├─► the answered change leaves the view
          └─► the plugin's fact events fire (origin = event.origin)
          │
          ▼
   store change ─► adapters re-run selectors ─► pure memoized reads ─► UI
```

Both engines publish the event for a session's own mutation before the
mutation's promise settles (see `DocumentEvent` in
`packages/engine/core/src/events/DocumentEvent.ts`, checked by
`runDocumentEventsConformance`). So when a verb's `await` returns, the fold has
already run: a verb never needs `waitFor`, a revision check or a refetch to see
its own write.

## Events

A capability exposes three kinds of events:

| Kind         | Meaning                              | Fired from                                                                                                                | Origin                            |
| ------------ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| Fact         | Something changed in the document    | Where the confirmed document event is applied: a mirror's `changed` callback, or the plugin's one listener for that event | `event.origin`                    |
| State change | A session value changed              | The plugin's `ctx.state.onChange` listener, comparing previous and next                                                   | none                              |
| Occurrence   | An operation ran, finished or failed | Where the operation reaches that point                                                                                    | a domain field when one is needed |

An event fires from exactly one place. Events report what happened; what
something is, is read through a getter. Details: [`events.md`](./events.md).

## Reads

- Reads are synchronous and pure: `get*`, `list*`, `is*`, `has*`, `can*`.
- A read returns the same object until its inputs change, so adapter
  selectors compare with `Object.is`. Build such reads with `memo` and
  `memoByKey` from `@embedpdf/core`, not with hand-written caches.
- Reads never start engine work. Loading starts with `ensureLoaded(page)`, or
  by the mirror itself.

## Reactions

Some plugins do not copy engine data but must respond to it: render
invalidates rasters, search re-runs its query, signature re-judges its
verdicts, actions clears its trigger cache. A reaction:

- subscribes with `ctx.listen(ctx.doc.events, …)` inside `connect`;
- ignores origin for what it does;
- only invalidates its own caches or re-runs its own operations. It never
  writes another plugin's data.

## Pure cores

A `core-*` package exists when a domain needs logic worth testing without a
kernel, engine or DOM: page geometry (`core-geometry`), the camera and scene
(`core-stage`), annotation gestures and hit-testing (`core-annotation`), the
toolbar solver (`core-ui`).

- Inputs in, decision out: plain functions, or `update(model, message)`
  returning the next session, the records it changed and the effects, as in
  `core-annotation`. No I/O, no clock, no randomness.
- A core never stores engine data. It reads the records it works on from its
  input and returns what it changed; the plugin decides where that lives.
- The plugin's controller performs the effects. A core does not call the
  engine.
- Cores know nothing about mirrors, overlays or events. They receive the data
  they need as arguments.
- A plugin's own pure functions live in its `model.ts`, not in a second
  `core/` folder.

## Plugin anatomy

```
packages/plugin/<name>/src/
  <name>.plugin.ts   the manifest: definePlugin({ id, token, scope, requires, optional, state, create })
  token.ts           createCapabilityToken(...), called once per package
  contract.ts        the public lens: capability interface, public types, the token re-exported
  host-contract.ts   optional: the host lens, the same token widened with createHostToken
  internal.ts        optional: framework-only helpers (a visibility boundary, not a bundle boundary)
  index.ts           the plugin factory and `export * from './contract'`
  model.ts           state shape, initial state, pure transitions, pure projections and folds
  controller.ts      create(ctx): builds the capability and returns { api, connect? }
  connect.ts         optional: registrations with sibling plugins, called from connect
  # larger plugins split the controller into areas:
  services/          plugin-private helpers shared by the areas
  read/ write/ sync/ tools/   one area per file, composed with composeApi
packages/plugin/<name>/test/   mirrors src/
```

Rules:

- The manifest holds no logic. Everything that registers with a sibling
  lives in `connect.ts` and runs inside `connect`.
- `create` returns `{ api, connect? }`, and `controller.ts` is where it is
  built.
- Every registration's `Unsubscribe` goes to `ctx.cleanup`, even when both
  sides close together.
- Tests mirror `src/` under `test/`.

Each file's role, the manifest fields, contracts and lifetime helpers:
[`plugins.md`](./plugins.md).

---

### Conventions Comments (`docs/conventions/comments.md`)

# Comments

A comment explains what the code cannot say: why it is this way, what must
stay true, what a specification requires, or what surprising input looks like.
It is written for a developer who has only this repository in front of them.

## Rules

1. **Self-contained.** A comment never depends on anything outside the
   repository. Never cite plans, decision numbers, phases, gates, work
   packages, internal section numbers, reviews, chats or tickets. If a rule
   matters, state it in the comment, or in a committed document and link that
   document by its repository path.
2. **Allowed references:**
   - external specifications with a section: `ISO 32000-2 §12.5.6.3`, `RFC 3161`;
   - the observable behavior of external products: `matches Acrobat: …`;
   - committed documents by path: `docs/conventions/state-and-sync.md`;
   - code symbols with `{@link Symbol}`.
3. **Present tense, current behavior.** No history: not "v2 behaviour",
   "matches v2", "the old mistake", "used to", "as before", "legacy",
   "interim" or "for now". The engine is "the engine", not "the v3 engine".
   Migration notes belong in changesets and the migration guide.
4. **No roadmap.** Future work is `TODO(#123): …` pointing at a GitHub issue,
   or it is not in the code.
5. **One owner per explanation.** Explain an invariant once, where it is
   enforced. Elsewhere, link to that symbol instead of repeating it.
6. **Public API is documented with TSDoc.** Every member of `contract.ts` and
   `host-contract.ts` gets:
   - a one-sentence summary;
   - the `PluginError` codes it rejects with and the events it fires, when it has any;
   - `@param`/`@returns` only when the names do not already say it.
7. **Length:**
   - inline comments: one to four lines;
   - a module header: what the module is responsible for, in at most about ten lines;
   - longer design explanations go into `docs/conventions/` or a package README, linked from the code.
8. **Tone.** No ALL-CAPS for emphasis; acronyms are fine. If something must
   never happen, write "must not", and back it with a type or a test.
9. **No commented-out code, no names of people, and no dates**, unless the date
   is part of a specification or protocol.
10. **Section dividers** (`// ── reads ──`) are allowed in files over roughly
    150 lines. Always use this format.

## Examples

| Instead of | Write |
|---|---|
| `// the snapshot can never settle stale (G5).` | `// Queue one more read, so the snapshot never settles on data older than the last invalidation.` |
| `// the nine members the authoring pattern needs (see the road-to-3.0 plan, §5.10)` | `// The guarded document handle, page geometry, mirrors, events and lifetime helpers, all bound to one instance.` |
| `// Faint reference cross, prominent live indicator (v2 feel).` | `// A faint reference cross and a prominent live indicator.` |
| `// feeding it a units conversion (the old mistake) shrank bodies` | `// A px-per-point scale here would shrink screen-anchored bodies at 100% zoom; this is a zoom ratio.` |
| `// Phase 3's shared ScriptHost swaps in by re-registering` | Describe what the code does now, and nothing else. |
| `// The ONE place the value changes` | `// The only place the value changes` |

## Enforcement

`pnpm check:comments` runs in CI. It reads comments with the TypeScript
scanner and rejects:
- plan and phase vocabulary, including numbered requirement labels;
- internal section numbers (`§` without an ISO, PDF or RFC citation);
- links to `.md` files that do not exist;
- version-history references;
- `TODO`s without an issue number;
- ALL-CAPS emphasis.

Justified exceptions go in `scripts/check-comments.allow`, one per line with a reason.

---

### Conventions Docs Architecture (`docs/conventions/docs-architecture.md`)

# Docs architecture — author once, render per integration

The law for how documentation is structured so that five integrations never
mean five copies. Framework parity makes this possible: "one page of prose +
per-framework code" is rendering, not aspiration.

Website source paths below are relative to `website/`; paths beginning with
`/docs` are public documentation URLs.

## The two halves

```
/docs
  /viewer      → the ready-made viewer (snippet + wrappers)
  /headless    → the adapter packages (@embedpdf/react|vue|svelte|angular)
  /engine      → engine choice (local wasm vs cloud), runtime, server
```

- **Viewer docs carry the shared integration switcher** for Vanilla JS, React, Vue,
  Svelte, and Angular. The config API and prose remain shared; installation and
  code examples resolve from the integration in the URL.
- **Headless docs carry the same integration switcher**, limited to React, Vue,
  Svelte, and Angular. There is one source page per feature: stage, render,
  selection, annotation, form, search, and so on. Prose is shared; code and API
  names render per framework.

Viewer and Headless share one persisted integration preference. Switching
products carries React, Vue, Svelte, or Angular with you. Vanilla JS falls back
to React when entering Headless because no Vanilla headless adapter exists.
The concrete URL remains the source of truth; the preference only resolves
variant-less courtesy routes.

## Author once, render per integration

One MDX file per topic. Integration-specific URLs are generated from it:

```
src/content/docs/headless/annotation.mdx
  → /docs/headless/react/annotation
  → /docs/headless/vue/annotation
  → /docs/headless/svelte/annotation
  → /docs/headless/angular/annotation
```

Viewer pages use the same fan-out model:

```
src/content/docs/viewer/quick-start.mdx
  → /docs/viewer/vanilla/quick-start
  → /docs/viewer/react/quick-start
  → /docs/viewer/vue/quick-start
  → /docs/viewer/svelte/quick-start
  → /docs/viewer/angular/quick-start
```

Why per-integration URLs instead of one URL + client switcher: SEO indexes
integration-specific content, links can pin an integration, analytics see per-
integration readership. The catch-all route strips the integration segment,
renders the shared MDX with the integration in context, and
`generateStaticParams` emits the page × integration matrix. The switcher in the
docs header just navigates to the sibling route (choice persisted, deep links
win over persistence).

Inside a page:

- `<Example name="annotation/quickstart" />` — renders the active integration's sample.
- `<Fw react>…</Fw>` — rare prose branches. If a page needs many of these,
  it's a smell: either the wording should be framework-neutral (see
  terminology map) or the page belongs in the per-framework fork set.
- The fork set is explicit and small: installation/scaffolding and SSR
  integration (Next/Nuxt/SvelteKit/Angular) are separate per-framework pages.

**Terminology map** (one page, linked from every headless page): hook =
composable = store = inject function; `<Viewer>` = `<EpdfViewer>`; etc.
Prose says "the selection hook" and means all four. Writing style guide:
never narrate JSX composition in shared prose.

## Code samples are real code, compiled in CI

The cure for docs rot: samples never live inline in MDX. They live as real
files, type-checked against the actual packages:

```
website/src/samples/
  package.json          → depends on @embedpdf/react (etc.), workspace:*
  react/annotation/quickstart.tsx
  vue/annotation/quickstart.vue
  svelte/annotation/quickstart.svelte
  angular/annotation/quickstart.ts
```

- `pnpm --filter @embedpdf/website-samples typecheck` runs in CI: an API
  change fails the build until the docs move with it. This is the docs
  equivalent of the consume gate.
- The MDX pipeline (each site's remark/rehype code-example plugins) reads the
  files at compile time and highlights them with shiki. An `<Example>` or
  `<Snippet>` keeps its code out of the compiled page: each framework's files go
  to a JSON file in `.next/cache/docs-code/`, keyed by the sample's name and a
  hash of the files, and the page only names the key and the frameworks that
  have a version. The page's server component (`RouteExample`) reads the
  route's framework's files at render (docs-kit `mdx/code-panels`). Inlined, four
  frameworks made page modules megabytes each, and the docs route, which
  compiles every page, ran the dev server out of memory.
- A missing sample for a framework renders an honest "not yet ported for
  {framework}" callout — driven by file presence, not hand-maintained flags.

## The reference is complete, and checked

Every headless plugin page ends with its reference: `## Methods` (everything
`use<Plugin>()` gives you), `## State` (everything `use<Plugin>State()` gives
you, with the getter that reads each value once), then
`## Settings`, `## Events` and `## Permissions`. The rows are written by hand,
in plain words, each linking to the section that shows it.

- Every public member of a plugin's capability is on exactly one page: a
  Methods row, a State row's getter, or an Events row. A plugin spread over
  several pages splits by its nouns (`annotation.selection.*` on Selecting).
- `docs/content/reference.mjs` says which pages document which capability.
  `docs/content/scripts/reference.mjs` reads each capability interface from its
  TypeScript source and fails on a member no page lists, a member on two pages,
  or a listed name the capability doesn't have. It runs in `docs:check`.
- The pages describe the API the code is moving to. Where they're ahead, the
  name is `pending` in the manifest, with why; the check fails once a pending
  entry is no longer needed, so the list only shrinks.
- The same check compares the tables with the declarations next to each
  contract, once a plugin has them: a State table's `Field` column with its
  `defineState()` fields, a Settings table's `Setting` column with its settings
  defaults (a row may name a group), and every `From CSS` column with
  `EPDF_VARIABLES` in `@embedpdf/web`, both ways.

## A page goes live when its code does

The pages describe the API the code is moving to, so a page isn't shown as
released until its code exists. Two machine checks decide it, per page and per
framework, and run in `docs:check`:

- `docs/content/scripts/snippets.mjs` compiles each framework's snippets against
  the packages in the same commit (React with `tsc`, Angular with `ngc` and
  `strictTemplates`, Vue with `vue-tsc`, Svelte with `svelte-check`) and writes
  `generated/snippet-status.json`. It never fails the build.
- `docs/content/scripts/publish-gate.mjs` writes `generated/live-pages.json`: a
  page is live for a framework when its snippets compile for it and none of the
  names its Methods, State or Events tables list is `pending`.

The viewer's pages go through the same gate, for five integrations: the four frameworks and
`vanilla`, a plain HTML page. Their snippets compile against `@embedpdf/viewer` at its source.
A `.vanilla.html` snippet's module scripts are taken out of the page and type-checked as
JavaScript, at the page's own line numbers. The reader's own DOM code (`event.target.value`) is
left alone, and the CDN script resolves to the viewer. The page's elements are checked too:
every `<embedpdf-viewer>` and `<epdf-*>` element must be declared in the viewer's types, every
`<embedpdf-viewer>` attribute must be a config key, a `slot` must name one of the viewer's, on a
child of the viewer, and a `<script src>` must be the CDN script or a path. The `js` and `ts`
blocks a viewer page shows are checked against `ViewerConfig` for every integration, as the
config is the same in each. `docs/content/scripts/viewer.mjs` holds these checks. A viewer page
whose prose promises what the viewer doesn't do yet lists those gaps in
`docs/content/viewer-pending.mjs`, each with why, and stays coming for every integration until
they're removed, however little code it has. In CI, `publish-gate.mjs --strict` (the docs gate
job in `.github/workflows/packages.yml`) fails when a check crashes or stops checking, or a
headless page's snippets don't compile; a viewer page that's coming fails nothing.

Live examples (`docs/content/samples/`) are complete apps, so every one must compile.
`docs/content/scripts/samples.mjs` checks them straight from `docs/content`, without syncing them
into a site or starting one, reports per framework (React with `tsc`, Angular with `ngc`, Vue
with `vue-tsc`, Svelte with `svelte-check`; Angular and Vue with `strictTemplates`) and fails on
an error:

```sh
pnpm --filter @embedpdf/docs-content samples          # every example
pnpm --filter @embedpdf/docs-content samples search   # one area's examples
pnpm --filter @embedpdf/docs-content samples viewer   # the ready-made viewer's
```

The ready-made viewer's examples (`samples/viewer/`) compile as its snippets do: against the
viewer at its source, for the four frameworks and `vanilla`, whose page's module scripts and
elements are checked as a `.vanilla.html` snippet's are (`viewer.mjs`).

Both checks share `scripts/compile.mjs`: each framework's compiler and options, and a run
directory of their own in the system's temporary directory, whose `node_modules` links to the
embedpdf.com site's, so several can run at the same time. (Never under a `node_modules` path:
`svelte-check` reports nothing for a file there.) Every run also compiles a canary per framework,
with a missing export, a missing module and a template naming nothing: a compiler that doesn't
report all three isn't checking, and the framework fails. An Angular file that declares a component,
directive or pipe without exporting it fails too: ngc can't type-check a template that uses it the
usual way, and then stops reporting template errors in every file. Vue snippets allow components they
don't import (a snippet may use a global or a Nuxt auto-import); examples don't. Each site's
`check:samples` runs the same example check on what it ships (`samples.mjs --root src/samples`):
the synced copy, in every framework, the cloud form included on cloudpdf.com.

### An example in each framework

```
samples/<area>/<name>.react.tsx       one file per framework…
samples/<area>/<name>.vue.vue
samples/<area>/<name>.svelte.svelte
samples/<area>/<name>.angular.ts      (root component `export class App`, selector `demo-root`)
samples/<area>/<name>.vue/App.vue     …or a directory, entry first (`App.svelte`,
samples/<area>/<name>.vue/Toolbar.vue   `App.tsx`; Angular's is `app.ts`)
samples/<area>/<name>.css             the one stylesheet every framework's version uses
```

React, Vue and Svelte import the stylesheet (`import './<name>.css'`, `'../<name>.css'` from a
directory). Angular names it in `styleUrl: './<name>.css'` with
`encapsulation: ViewEncapsulation.None`, and writes its template inline. The code view shows each
framework's files with the stylesheet after the entry. In the demo builds both forms become the
same stylesheet, scoped to the example's class (`src/lib/sample-stylesheets.ts` in each site);
the Angular pass compiles in the browser (JIT), so it first applies Angular's JIT transform,
without which `input()`, `output()`, `model()` and `viewChild()` do nothing
(`src/lib/angular-demo-samples.ts`).

### A viewer example

```
samples/viewer/<page>/<name>.vanilla.html   a plain HTML page: markup and module scripts
samples/viewer/<page>/<name>.react.tsx      …and the four frameworks, as above
samples/viewer/<page>/<name>.cloud.react.tsx   cloudpdf.com's own version, where a swap can't say it
```

The page shows one with `<Example name="viewer/<page>/<name>" />`, like any other. The viewer fills
its box, so a viewer example gives it `height: 100%` and the docs give the box: a frame that suits
the docs column, or a bigger one with `size="tall"`, or `size="wide"`, as tall and on a wide screen
over the table of contents' column too (`src/components/docs/example.tsx`, the kit's
`CodeExampleCard` `frame`). A page may show several: each starts its own viewer, when it scrolls
near. Its document is a `[!doc-source]` block holding a constant (`const ebook = '…';`, in a plain
HTML page inside its module script), which cloudpdf.com's copy gives in the cloud viewer's form.

The plain HTML page loads the viewer from its CDN script, as a reader's does. Its demo
(`src/lib/vanilla-demo-samples.ts`) is the page as a module: the markup goes into the demo's
element, each script's imports move to the module's top and the rest runs on every mount, finding
elements in its own demo only, and the CDN script is the site's own build of it
(`@embedpdf/viewer/snippet` on embedpdf.com, `@cloudpdf/viewer/snippet` on cloudpdf.com). Nothing
else may load from another origin. The viewer's demos use its packages' builds, as a reader's app
does: rebuild a viewer package you changed before `build:demos`.

The checks and the demos read the adapters' source through their workspace `exports`: Vue's
point at `./src/<entry>.ts` as plain strings, like React's; Svelte's name the same file under
`types`, `svelte` and `default`. Never only under `development`, which the demo builds don't use.
The checks map `@embedpdf/angular/*` to its source (`compile.mjs`); the Angular demos use its
build.

A page that isn't live shows its title and a notice that it describes 3.0 for
that framework, linking to the React version when that one is live; it stays out
of `llms.txt`, that framework's search index and search engines. With
`DOCS_PREVIEW=1` (the preview site, and `next dev` unless `DOCS_PREVIEW=0`),
every page is live under a banner. The sites read the gate through
`@embedpdf/docs-kit/publish`. Both generated files are built, never committed.

## The support matrix is generated, not written

During the rollout (React complete → Vue/Svelte/Angular incremental), every
vertical's page shows its framework support honestly. The matrix derives from
two machine sources: the adapter's exports map (does the vertical exist?) and
sample presence (is it documented?). Angular's `check-parity.mjs` `PENDING` set
is the same data — one source of truth, surfaced in docs.

## v2 docs afterlife

- Current v2 site: frozen static build at `v2.embedpdf.com`, banner linking
  to current docs. Never rots, never maintained.
- 301 map from the old 200-page URL space into the new tree lives in
  `next.config.ts` `redirects()` — written once at launch, SEO preserved.
- Docs version switcher: just a link to the archive. No in-tree versioning.

## Rollout order

1. `/docs/viewer` — launch the ready-made Viewer integrations first.
2. `/docs/headless/react/*` — the complete vertical set, proving the
   author-once machinery.
3. Vue/Svelte/Angular routes go live per vertical as adapters land — the
   generated matrix keeps the gaps honest instead of hidden.

---

### Conventions Engine Host Isolation (`docs/conventions/engine-host-isolation.md`)

# Engine-host isolation

Thread confinement makes parallel PDFium use correct; it does not make native
crashes safe. Host isolation moves the thread-confined `WorkerThreadPool` into
one or more supervised child processes so an engine crash does not kill the API
process. Enable it with `CLOUDPDF_ENGINE_ISOLATION=host`; use
`CLOUDPDF_ENGINE_SHARDS` to partition documents across multiple supervised
hosts. The thread-ownership rules in
[`runtime-confinement.md`](./runtime-confinement.md) still apply inside every
host.

The implementation lives in `cloudpdf/server/src/runtime/EngineHostClient.ts`,
`cloudpdf/server/src/runtime/ShardedEnginePool.ts`, and
`cloudpdf/server/src/runtime/engine-host-entry.ts`.

## Threat model: what a compromised engine can reach

PDFium parses attacker-controlled bytes; assume an eventual RCE in the engine
process and design for containment, honestly labelled.

### Credential-exposure reduction

- The host child is forked with a whitelisted environment: no
  `CLOUDPDF_DB_URL`, JWT secret, license key, or object-store credentials.
  `hostEnvWhitelist` applies the allowlist, integration tests assert the child
  environment, and `NODE_OPTIONS` is stripped so an inherited `--inspect`
  cannot open a debug port in the engine process.
- This is exposure reduction, not a hard security boundary. The engine is a
  same-container, same-UID child. Whether it can read the API process's memory
  or `/proc/<pid>/environ` depends on the kernel's ptrace policy
  (`kernel.yama.ptrace_scope`, a node-level sysctl in Kubernetes). `HOME` and
  the container filesystem remain shared. Marking the API process non-dumpable
  (`PR_SET_DUMPABLE=0`) would close same-UID `/proc` access independently of
  Yama, but is not currently part of this boundary.
- Durable state integrity is a hard property: a lying engine can corrupt its
  own outputs, but the write pipeline's generation fence and version CAS keep
  it from silently blessing stale state as committed.

### Reachable surfaces

- The pod network. NetworkPolicy can narrow pod egress, but the engine can
  still reach API-process ports inside the pod.
- The base and layer cache files it renders from, plus its IPC channel to the
  parent. Treat rendered output as untrusted content.
- The kernel syscall surface, minus seccomp `RuntimeDefault`. A sandbox runtime
  such as gVisor or Kata, configured through `runtimeClassName`, strengthens
  pod-to-node isolation at syscall-emulation cost. It applies to the whole pod;
  it does not isolate the engine from the API process inside that pod.

### Non-boundaries

- In-process Node sandboxing, including the permission model and frozen
  intrinsics, is not a security boundary against native code.
- A sidecar split does not by itself create an intra-pod network boundary:
  ordinary Kubernetes NetworkPolicy cannot distinguish containers in the same
  pod. Re-evaluate a separate-pod engine tier if a hard network and identity
  boundary becomes a requirement.

## Crash and overload blast radius

- A host crash rejects in-flight work retryably and triggers a backoff-capped,
  sub-second respawn.
- Multiple shards reduce the resident-document blast radius because each host
  owns only its rendezvous-hashed partition. Any persistently unavailable shard
  makes the pod unready; requests do not fail over to a different shard inside
  the same pod.
- Repeated sole-suspect crashes are journaled and quarantined by
  `(base_sha, engine_build)`.
- Admission control sheds overload with retryable 503 responses instead of
  queueing without a bound.

Operational rollout, sizing, recycling, and metrics live in
`cloudpdf/server/deploy/helm/DEPLOY.md`.

---

### Conventions Engine Services (`docs/conventions/engine-services.md`)

# engine-services conventions

`@embedpdf/engine-services` contains the synchronous, runtime-agnostic engine logic shared by
`@embedpdf/engine` and `@cloudpdf/server`.

The package should read like a small framework: a developer should know what a file does from its
directory before opening it. Paths below are relative to
`packages/engine/services/` unless they start at the repository root.

## Top-level structure

```txt
src/
  runtime/           Low-level helpers for @embedpdf/engine-runtime calls.
  shared/            Generic helpers used across layers.
  document-session/  Lifetime and identity state for one open document.
  features/          Business-level document capabilities.
  worker-host/       Worker wire dispatch and request orchestration.
```

Dependencies flow downward:

```txt
runtime/shared -> document-session -> features -> worker-host
```

`document-session/` must not import from `features/`. Feature code may use a `DocumentSession`;
the session layer should stay focused on open-document state, page identity, page handles, and
revision bookkeeping.

## Feature modules

Each feature follows the same shape:

```txt
feature-name/
  FeatureReader.ts
  FeatureMutator.ts      # only when the feature changes document state
  internal/
    ...
  index.ts
```

Rules:

- Use `Reader` for read-only orchestration.
- Use `Mutator` for business-level document changes.
- Put helper functions, registries, per-subtype codecs, and low-level runtime details in
  `internal/`.
- `index.ts` exports only the feature's public orchestration surface.
- Avoid root-level helper files beside a reader or mutator; if it is not an entry point, it belongs
  under `internal/`.

## Naming

- Public orchestration classes use `PascalCase.ts`: `MetadataReader.ts`, `PagesMutator.ts`.
- Internal helper modules use descriptive verb phrases: `readMetadataText.ts`,
  `writeTextMarkupAnnotation.ts`, `computeMutationImpact.ts`.
- Avoid generic names like `util.ts`, `helpers.ts`, or `registry.ts` unless they live under an
  already-specific folder and still read clearly.
- Use `runtime` or `@embedpdf/engine-runtime` terminology. Do not use upstream project names as
  directory names.

## Worker boundary

`worker-host/WorkerHost.ts` is the public host entry used by local workers, inline transports, and
server worker threads. It should own:

- request envelopes;
- abort controller tracking;
- resolve/reject serialization;
- session lookup and close/shutdown routing.

Feature behavior belongs in `features/`, not in worker adapters.

This package runs where PDFium runs: in the engine's worker, the server's worker threads, or the
page itself when there is no `Worker`. A page's main thread never imports it otherwise. The
local engine imports it, with `@embedpdf/engine-runtime`, only in
`packages/engine/main/src/transport/InlineTransport.ts`, loaded with `import()` when an engine
boots in the page (`packages/engine/main/test/entry-graph.test.ts` checks it). What both engines
need beside a document handle, such as `EventHub`, lives in `@embedpdf/engine-core/runtime`.
The checks it runs before a write (a write's values against its kind's schema, the import
planners) come from `@embedpdf/engine-core/validation`, the entry built with zod; the page's
entries reach no zod (`packages/engine/core/test/entry-graph.test.ts`). A write is prepared,
checked and derived; a viewer predicts it with the same preparation and derivation without
schema validation, and the check only accepts or refuses, so this package writes what the viewer
predicted or refuses it.

## Mutation safety

- Every job whose effect is `write` or `contentWrite` runs as one layer transaction, begun and ended
  in `WorkerHost.run` (a signing's `signatures.complete` is the exception: it replaces the
  document's bytes instead). The transaction commits in `finishMutation`, before the layer is saved; any throw before
  that aborts it, and the document and the derived caches are as they were. A
  writer never begins, commits or rolls back on its own.
- A commit or an abort that fails leaves the session unusable: every later job is refused with
  `DocNotOpen`, and the document has to be opened again.
- Mutators still validate caller input and check cancellation before the first native write: a
  refusal costs no copies, and the error names the caller's mistake.
- A check that depends on the document runs inside the write, against what the write reads: the
  caller's authority over an annotation's owner (`AnnotationAuthority`), the members of a thread a
  delete takes. No read job runs before a write to check it.
- Writers find annotations from the page's dictionaries (raw handles), so a write never parses a
  page's content unless it writes that content (flatten, redaction).
- A write that moves entries of a page's `/Annots` (a delete, a reorder, a flatten, a redaction, a
  merged field's split) calls `promoteInlineAnnotations` first, before it loads the page: an inline
  annotation is named by its position only while no entry has moved.
- A batch is all or nothing too: redaction apply and page flatten throw when a page fails or the
  call is cancelled, and their per-page results only say `applied` or `unchanged`.
- Writes bump `cacheSeq` (derived caches) as they go; `finishMutation` bumps `editsSeq` (unsaved
  edits, the signing version) once per committed write.

## Adding a feature

When adding a new worker capability:

1. Add the wire request/response types in `@embedpdf/engine-core`.
2. Add a `Reader` or `Mutator` under `features/<name>/`.
3. Put implementation details under `features/<name>/internal/`.
4. Add a small handler path in `worker-host/`.
5. Add local/server/cloud adapter changes only where the public engine API requires them.
6. Cover the behavior with conformance tests or focused package tests.

---

### Conventions Events (`docs/conventions/events.md`)

# Events

A capability's events report what happened. What something is, is read
through a getter. This document covers the three kinds of events, where each
one fires, how events are created and named, and how a fact event says where
its change came from.

## Three kinds

| Kind         | Meaning                                             | Fired from                                     | Origin                                | Examples                                                                                                                                             |
| ------------ | --------------------------------------------------- | ---------------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Fact         | Something changed in the document                   | Where the confirmed document event is applied  | `event.origin`, as the engine sent it | metadata `onUpdated`; form `onValueChanged`, `onFieldCreated`; annotation `onCreated`, `onDeleted`; signature `onSigned`; redaction `onApplied`      |
| State change | A session value changed                             | The plugin's one `ctx.state.onChange` listener | none: session state has no origin     | shell `onSurfaceOpened`; stage `onZoomChanged`, `onPageChanged`; search `onActiveHitChanged`, `onProgressChanged`                                    |
| Occurrence   | An operation ran, was requested, finished or failed | Where the operation reaches that point         | a domain field when one is needed     | search `onStarted`, `onCompleted`, `onFailed`; stage `onMotionEnded`; form `onValidationRejected`; commands `onExecuted`; annotation `onWriteFailed` |

### Fact events

A fact event fires where the plugin applies the confirmed document event:

- in a mirror's `changed` callback, for data the plugin mirrors
  (`packages/plugin/form/src/sync/fields.ts`,
  `packages/plugin/metadata/src/controller.ts`);
- in the plugin's one `ctx.listen(ctx.doc.events, …)` handler for that event
  type, for data it does not mirror (`packages/plugin/redaction/src/sync/document-events.ts`).

Because the confirmed event is the only source, the same event fires for this
session's writes, the document's scripts and other sessions, and it fires
once. A verb never emits a fact event itself.

```ts
// packages/plugin/form/src/sync/fields.ts, inside the mirror spec
changed: ({ cause, event, previous, next }) => {
  if (cause === 'load') {
    if (next.snapshot) events.resynced.emit({ snapshot: next.snapshot });
    return;
  }
  if (!event || !('origin' in event)) return;
  const { origin } = event;
  switch (event.type) {
    case 'forms.valueSet':
      events.valueChanged.emit({ field: event.field, origin });
      return;
    // …
  }
},
```

### State-change events

A plugin derives all of its state-change events in one `ctx.state.onChange`
listener, from the value before and after each committed change. A new verb
therefore cannot forget to announce, and a verb never emits a state-change
event itself.

A state-change event is derived from state even when it reads like progress.
If a value is state (a getter returns it, and it is in the page's State
table), its event fires from the listener when the value changes, never from
the operation that moves it. Search's `progress` is state, so
`onProgressChanged` fires when `progress` changes: once for each slice of
pages a search gets through, and back to none when a new search starts or
the session is cleared. The event and `getProgress()` then never disagree,
whichever verb or reset changed the value.

```ts
// packages/plugin/shell/src/controller.ts
ctx.state.onChange(({ previous, next }) => {
  if (previous.openMenus !== next.openMenus) {
    for (const id of previous.openMenus) {
      if (!next.openMenus.includes(id)) menuClosed.emit({ id });
    }
    for (const id of next.openMenus) {
      if (!previous.openMenus.includes(id)) menuOpened.emit({ id });
    }
  }
});

// packages/plugin/search/src/controller.ts
ctx.state.onChange(({ previous, next }) => {
  // A slice moves it forward; a new search or clear() puts it back to none.
  if (previous.progress !== next.progress) {
    progressChanged.emit({ ...next.progress, hitCount: next.hits.length });
  }
  // …
});
```

### Occurrence events

An occurrence fires where the operation reaches the point it reports: the
search scan emits `onStarted` when it starts and `onCompleted` when it is
complete; the stage's camera animation emits `onMotionEnded` where a tween or
fling ends; the form write path emits `onValidationRejected` when the
document's scripts refuse a value. An occurrence that needs to say who asked
carries a domain field for it (the actions plugin's `ActionContext.origin`),
not an `EventOrigin`.

## Rules

- An event fires from exactly one place.
- A verb never emits a fact or state-change event.
- When a session value a getter returns is announced, it's by a state-change
  event, never by an occurrence, even when the value is an operation's
  progress. Not every value needs an event: the state hook follows them all.
- A refusal before an operation started fires nothing: it only rejects. An
  operation's failure event (`onFailed`) is for work that started.
- Loads and reloads are announced once, as `onResynced`. They never produce
  per-item fact events.
- Events are for things that happened. A UI never keeps its own copy of state
  by listening to events; it reads through a getter and a selector. A late
  subscriber that needs the current value calls the getter.
- Payloads are plain, serializable objects with `readonly` fields.

## Creating events

A plugin creates each event with `ctx.events.source<T>()`:

```ts
const updated = ctx.events.source<MetadataUpdatedEvent>();
// { on: EventHook<T>, emit(event: T): void, dispose(): void }

const api: MetadataCapability = {
  // …
  onUpdated: updated.on,
};
```

- The capability exposes only `.on`, typed `EventHook<T>`. `emit` and
  `dispose` stay inside the plugin, so the emitting half cannot be reached
  through the contract.
- The kernel disposes every source when the instance closes.
- Plugins do not call `createEventHook` directly; it exists for the kernel
  and the adapters.

Delivery (`packages/core/main/src/event-hook.ts`):

- `emit` calls the listeners synchronously, over a snapshot of the listener
  set taken at the start of the emit.
- A throwing listener is reported and never breaks the emitting operation or
  the other listeners.
- `on(listener, { signal })` ties the subscription to an `AbortSignal`, and
  returns an `Unsubscribe`.
- There is no replay and no cached value.

## Naming

- A capability event is `on<Subject?><PastParticiple>`: `onFieldCreated`,
  `onZoomChanged`, `onSurfaceOpened`, `onApplied`.
- A value that changes is announced as `on<Value>Changed`: `onZoomChanged`,
  `onLocaleChanged`, `onActiveHitChanged`.
- A load or reload is `onResynced`.
- The payload type is named after its event: `onFieldCreated` →
  `FieldCreatedEvent` (usually prefixed with the plugin:
  `MetadataUpdatedEvent`, `SearchCompletedEvent`).

The full vocabulary is in [`naming.md`](./naming.md#capability-vocabulary).

## Origin

Every fact event carries `origin: EventOrigin`: the engine event's own
`origin`, passed on as it is. No plugin builds or translates an origin
(`scripts/check-architecture.mjs` flags an object literal with `sessionId`
and `sub`).

```ts
// @embedpdf/engine-core, re-exported from @embedpdf/core
interface EventOrigin {
  kind: 'local' | 'remote'; // this engine instance, or another session
  sessionId: string; // the engine session that made the change
  sub: string | null; // the authenticated user behind it (cloud); null locally
  ts: number; // when it happened
  serverId: number | null; // the server's audit row, the resume cursor
  tx?: { id: string; index: number; count: number }; // one of several committed together
}
```

- `kind: 'local'` means this engine instance, not this user: the same user in
  two tabs is two sessions, and each tab sees the other's changes as
  `remote`.
- Ask `kind` or `sessionId` for "is this my own action?" (undo, reconciling
  optimistic state). Ask `sub` for attribution.
- Handlers that update state ignore origin (see
  [`state-and-sync.md`](./state-and-sync.md#rules)); origin is for the few
  features that care about provenance.
- `stream.desynced` carries no origin: it is a transport notice, not a
  mutation. A mirror turns it into a reload and an `onResynced`.
- An event that a verb can cause as well as a document event carries
  `origin: EventOrigin | null`, with `null` for the verb: render's
  `onInvalidated` after `invalidate()`.

## Resynced

`onResynced` fires when a mirror's load lands: the first load, a `refresh()`,
and every reload after `stream.desynced`, `document.versioned` or a fold that
asked for one. Its payload says what was read, never what changed:

| Plugin     | Payload                         |
| ---------- | ------------------------------- |
| metadata   | `{ metadata }`                  |
| form       | `{ snapshot }`                  |
| annotation | `{ pages: 'all' \| PageRef[] }` |

A subscriber that cares about the new content reads it through the getters.

## Subscribing

- **Inside a plugin**: `ctx.listen(source, listener)`, where `source` is an
  `EventHook` (`annotation.onCreated`) or anything with `subscribe`
  (`ctx.doc.events`). The kernel owns the unsubscribe; subscriptions to
  siblings are made in `connect`.
- **In React**: `useCapabilityEvent(Token, (capability) => capability.onX, handler)`,
  or the per-plugin helper (`useMetadataEvent`, `useSearchEvent`, …).
  Angular has `injectCapabilityEvent`. Both subscribe for the component's
  lifetime.
- **Anywhere else**: `const off = capability.onX(handler)`, or pass
  `{ signal }` and abort it.

The kernel's own document lifecycle events live on the documents capability
(`DocumentsToken`): `onOpened`, `onOpenFailed`, `onLocked`, `onClosed`,
`onActiveChanged`, `onPagesChanged` and `onUnsavedChangesChanged`.

---

### Conventions Naming (`docs/conventions/naming.md`)

# Naming

Names are the first documentation a reader sees. These rules exist so that any
file in the repository reads the same way, and so that a name can be trusted
without opening its definition.

Package names follow a separate law: [`packages.md`](./packages.md).

## Principles

1. **A name says what the value is in the domain**, not its type or storage:
   `annotation`, `field`, `pane`, `hit`. Not `a`, `f`, `obj`, `data`.
2. **Spell words out.** Abbreviations are allowed only when the glossary below
   lists them.
3. **One name per concept, everywhere.** If the engine calls it a `PageRef`,
   nothing calls it a page id, pointer or address.
4. **A name's length follows its reach.** Exported and module-level names must
   stand alone. A two-line callback may use a short word (`page`, `hit`), never
   a letter.

## Identifiers

| Kind                             | Rule                                                                                                                                                       | Example                                                                        |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Single letters                   | Only `i`/`j` as numeric loop counters, `x`/`y` as coordinates, and `_` for an ignored parameter.                                                           | `for (let i = 0; …)`, `{ x, y }`                                               |
| Comparators and equality helpers | `left`/`right`, never `a`/`b`.                                                                                                                             | `(left, right) => left.index - right.index`                                    |
| Callback parameters              | Name what the callback receives.                                                                                                                           | `useSelector(SearchToken, (search) => search.getActiveHit())`                  |
| Boolean values                   | Adjectives or participles for state: `open`, `placed`, `resting`, `readOnly`.                                                                              | `pane.open`                                                                    |
| Boolean functions                | `is*`, `has*`, `can*`, `should*`, `would*`.                                                                                                                | `canEdit()`                                                                    |
| Permissions                      | Always `can*`, as a capability method; a component that needs it reactively reads it with a selector into a local of the same name. Never a renamed field. | `const canEdit = useSelector(PageEditToken, (pageEdit) => pageEdit.canEdit())` |
| Functions                        | Start with a verb. `create*` builds an instance, `to*` converts, `*Of(value)` is a pure lookup or projection.                                              | `createPane`, `toPageRef`, `regionsOf(mark)`                                   |
| Constants                        | `UPPER_SNAKE` only for module-level constants holding primitives or frozen data.                                                                           | `DEFAULT_CHROME`, `RERUN_DELAY_MS`                                             |
| Units                            | Put the unit in the name when it is not obvious.                                                                                                           | `delayMs`, `thresholdPx`, `sizePt`, `angleDeg`                                 |
| Unused parameters                | Prefix with `_`. Never use `_` for "private".                                                                                                              | `(_event, page) => …`                                                          |
| Declarations                     | `const` unless reassigned; `let` otherwise; never `var`.                                                                                                   |                                                                                |
| Generic parameters               | `T`, `K`, `V` only in fully generic helpers; name them elsewhere.                                                                                          | `memo<Inputs, Result>`, `PluginDef<State, Capability>`                         |

## Glossary

These are the only abbreviations, and the exact meaning of words that are easy
to mix up.

| Word                                     | Means exactly                                                                                                                                                                        | Never used for                                                                            |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| `ctx`                                    | the plugin's `PluginContext`                                                                                                                                                         | action, command or canvas contexts (`actionContext`, `commandContext`, `canvasContext`)   |
| `doc`                                    | a `DocumentHandle`                                                                                                                                                                   | document metadata (`documentInfo`), DOM documents (`ownerDocument`)                       |
| `api`                                    | the capability object a controller is assembling                                                                                                                                     | a capability received from elsewhere (name it after its plugin: `annotation`, `stage`)    |
| `id` / `ids`                             | an opaque string identifier                                                                                                                                                          | object numbers, refs, keys                                                                |
| `ref`                                    | a typed engine reference (`PageRef`, `AnnotationRef`, `FormFieldRef`)                                                                                                                | DOM refs (`elementRef`), ids                                                              |
| `key`                                    | a string derived from a ref for map lookups (`annotationKey`, `encodePageKey`, `FieldKey`)                                                                                           | refs, ids                                                                                 |
| `dto`                                    | an engine DTO, only in code that converts DTOs                                                                                                                                       | model records                                                                             |
| `page`                                   | a `PageRef`                                                                                                                                                                          | a display index (`pageIndex`), an object number (`pageObjectNumber`), layout (`pageInfo`) |
| `pageIndex`                              | a zero-based display position                                                                                                                                                        | identity                                                                                  |
| `pageObjectNumber`                       | the scalar object-number foreign key (type `PageObjectNumber`)                                                                                                                       |                                                                                           |
| `objectNumber`                           | the number inside a ref of kind `'objectNumber'` (`PageRef`, `AnnotationRef`, `FormFieldRef`), and on a record that is the object itself (a form widget); the type says which object | a loose number whose object isn't obvious (name it: `pageObjectNumber`)                   |
| `record`                                 | an entry of a mirror (engine data held by a plugin)                                                                                                                                  | DTOs, overlay entries                                                                     |
| `event`                                  | a document or capability event                                                                                                                                                       | a DOM event in code that also handles document events (`domEvent`)                        |
| `dx`, `dy`                               | coordinate deltas                                                                                                                                                                    |                                                                                           |
| `url`, `uri`, `dpr`, `min`, `max`, `pdf` | themselves                                                                                                                                                                           |                                                                                           |

Spell out everything else: `error` (not `err`, `e`), `options` (not `opts`),
`config` (not `cfg`), `previous` (not `prev`), `current` (not `cur`), `index`
(not `idx`), `result` (not `res`), `callback` (not `cb`), `element` (not `el`),
`message` (not `msg`), `definition` (not `def`), `selection` (not `sel`).

## Types

| Suffix                            | Use                                                                             |
| --------------------------------- | ------------------------------------------------------------------------------- |
| `XState`                          | a plugin's session state                                                        |
| `XCapability` / `XHostCapability` | the public / sibling-plugin capability                                          |
| `XConfig`                         | registration configuration (`xPlugin(config)`)                                  |
| `XOptions`                        | per-call options; extends `OperationOptions` when the call does work            |
| `XInput`                          | the input of a create verb                                                      |
| `XPatch`                          | a partial update                                                                |
| `XInfo`                           | a public read shape that is not the internal record                             |
| `XRef` / `XKey` / `XId`           | as in the glossary                                                              |
| `XEvent`                          | an event payload, named after its event: `onFieldCreated` → `FieldCreatedEvent` |
| `XRecord`                         | a mirror entry                                                                  |

- Use `interface` for object shapes in contracts, and `type` for unions,
  aliases and mapped types.
- Contract fields and state fields are `readonly`.
- Getters return `T | null` for "absent". Optional inputs and optional data
  fields use `?:`. One API never uses both `undefined` and `null` for the same
  meaning.

## Discriminated unions

- **`type`** tags things that happen: messages, events, effects.
- **`kind`** tags things that are: geometry, refs, borders, drafts.
- Tag values use one style per union (kebab-case or camelCase), never both.

## Capability vocabulary

| Verb                                   | Meaning                                                                                  |
| -------------------------------------- | ---------------------------------------------------------------------------------------- |
| `get*`, `list*`, `is*`, `has*`, `can*` | Read current state. Always synchronous.                                                  |
| `read*`, `load*`                       | Read from the engine. Always returns a promise.                                          |
| `ensureLoaded(page)`                   | Start loading if needed. Returns `Promise<void>`.                                        |
| `refresh()`                            | Reload engine data. Returns `Promise<void>`.                                             |
| `set*` / `update*`                     | Change a value: `set` replaces, `update` merges.                                         |
| `create*` / `delete*`                  | Make or destroy a document object.                                                       |
| `add*` / `remove*`                     | Put into or take out of a collection.                                                    |
| `open` / `close` / `toggle`            | Surfaces, menus, documents.                                                              |
| `begin*` / `end*`                      | Bracket a gesture or an edit session.                                                    |
| `register*`                            | Add an extension. Always returns `Unsubscribe`.                                          |
| `on*` on a capability                  | An `EventHook`, named `on<Subject?><PastParticiple>`: `onFieldCreated`, `onZoomChanged`. |
| `on*` on a registration object         | A callback the capability calls, such as `InteractionHandler.onDown`.                    |

`i18n.t` is the one single-letter member, following the universal i18n convention.

## Files and folders

- Files are kebab-case and hold one area each, named for the area:
  `write/values.ts`, `read/widgets.ts`.
- Plugins follow the anatomy in [`plugins.md`](./plugins.md).
- Tests mirror `src/` under `test/`. Test names are sentences that describe behavior.

## Enforcement

ESLint enforces the mechanical parts:

- `id-length` with the single-letter exceptions above;
- `id-denylist` for the spelled-out words above;
- `@typescript-eslint/naming-convention` for casing.

Review covers the rest.

---

### Conventions Packages (`docs/conventions/packages.md`)

# Package naming — the law

[`plugins.md`](./plugins.md) is the law for plugin authors,
[`naming.md`](./naming.md) for identifiers inside the code, and
[`tooling/build/README.md`](../../tooling/build/README.md) for
how packages build. This file is the law for where a package lives and what it
is called. Every name derives from the tree; if you have to debate a name, the
tree is wrong.

## The three clauses

1. **A package's npm name is its repo path with dashes.**
   `packages/plugin/stage` → `@embedpdf/plugin-stage`,
   `packages/core/geometry` → `@embedpdf/core-geometry`,
   `packages/engine/services` → `@embedpdf/engine-services`.
2. **`packages/<group>/main` is the group's namesake: `@embedpdf/<group>`.**
   `core/main` is the kernel (`@embedpdf/core`); `engine/main` is the default
   local-wasm engine (`@embedpdf/engine`). Groups without a principal package
   simply have no `main/`.
3. **`framework/` packages are bare-named** — `@embedpdf/react`, `/vue`,
   `/svelte`, `/angular`, `/web`. They are the marquee surface an app installs;
   the group prefix would only be noise on npm.

## The groups

| Group        | What belongs there                                                                                                                                                                                                                                                                                                                                                                      |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core/`      | Framework-free, DOM-free client logic (the Rust-portable layer): kernel, geometry, feature cores, acrojs, js-sandbox. Narrow `lib` in tsconfig is the purity guard — never widen it to accommodate a dependency.                                                                                                                                                                        |
| `engine/`    | The PDF engine family: contract (`core`), default engine (`main`), services, wasm/napi runtime (`runtime` + its `npm/*` target sidecars).                                                                                                                                                                                                                                               |
| `plugin/`    | Kernel plugins, framework-free. Directory drops the `plugin-` prefix (the group says it); npm names keep it.                                                                                                                                                                                                                                                                            |
| `framework/` | All framework-specific code. Nothing framework-flavored exists outside this group — where "framework-flavored" means adapter code (hooks/components for composing viewers); see `viewer/`.                                                                                                                                                                                              |
| `viewer/`    | The full-viewer product line: the one React chrome (`chrome`, private), its Preact-compiled delivery (`main` → `@embedpdf/viewer`: the `<embedpdf-viewer>` element + `EmbedPDF.init`), and thin per-framework wrappers (`react`, `vue`, `svelte`, `angular`). Products built on `framework/`, not adapters: a viewer package exports the finished viewer, never composition primitives. |

Examples (`examples/*`) and internal tooling (`tooling/*`) are not part of the
law's namespace: examples are `@embedpdf/example-<dir>` and private; tooling is
`@embedpdf/tooling-<dir>` and private.

## `cloudpdf/` — the same law, the other scope

`cloudpdf/` holds the CloudPDF (`@cloudpdf/*`) tree, and the directory name is
the scope. The three clauses apply verbatim with the scope swapped:
`cloudpdf/viewer/react` → `@cloudpdf/viewer-react`, and `cloudpdf/viewer/main`
is the group's namesake `@cloudpdf/viewer`. One law, two scopes — a CloudPDF
package is never named by a separate rule.

`cloudpdf/` is a scope boundary, not a license boundary. The license line is
per-package and runs by what a package is, not where it lives: libraries are
Apache-2.0 like the rest of the repo; the deployable server product
(`cloudpdf/server`) is Fair Source (FCL-1.0-ALv2), the repository's only
non-Apache package. See `LICENSING.md`. Do not infer a package's license from
its directory; read its own `LICENSE`.

What decides whether a path gets a group level is one line:

> **A directory level exists to hold siblings.**

That is why every `packages/` group is grouped (each has siblings) and why
`cloudpdf/engine`, `cloudpdf/server`, `cloudpdf/sdk`, `cloudpdf/contract`
sit flat: a group level over a lone package carries no information.
`cloudpdf/viewer/` is grouped because the viewer product line has five members
(`main` plus one wrapper per framework). When a flat CloudPDF package gains a
sibling it regroups then — repo paths are internal, so unlike npm names a move
costs nothing but the churn.

Two consequences worth stating, because both have already caught us:

- **Nothing may reconstruct a package's directory from its npm name.** Clause 1
  maps path → name, and that is not invertible without knowing the group list.
  Ask the workspace instead (`pnpm ls -r --depth -1 --json` yields name + path);
  the release workflows do.
- **Cross-scope names collide on their last segment.** `@embedpdf/viewer-react`
  and `@cloudpdf/viewer-react` are the two doors of one product — deliberately
  parallel — so anything keyed on the short name (release assets, cache keys,
  directories) must carry the scope.

Viewer wrappers keep the `viewer-` prefix in both scopes; clause 3's bare
naming is for `framework/` adapters only. `@cloudpdf/react` is free on npm but
would read as the cloud twin of `@embedpdf/react` — a composition adapter —
when it is in fact a finished viewer.

## Corollaries

- Adding a package = choosing its group; the name falls out. A package that
  fits no group is a design conversation, not a naming one.
- Renames after 3.0.0 ships are breaking changes; before that they are free.
  This law was locked while everything was at 0.0.0 — keep it locked.
- The changeset `fixed` group in `.changeset/config.json` enumerates every
  `@embedpdf/*` publishable — one version for the whole SDK, engine included.

---

### Conventions Permissions (`docs/conventions/permissions.md`)

# Permissions — the law for authorization-aware plugins

[`plugins.md`](./plugins.md) is the law for a plugin's shape; this file is the law for how a
plugin answers **"may this session do that?"** — so every viewer built on the
plugins, ours or a host's, renders a truthful UI instead of offering actions
the engine will refuse.

## The four layers, one direction

```
1. Engine / server        enforces   scope + collab grammar + PDF bits (ScopeGuard, routes,
                                     and per-record checks inside the worker's write)
2. Security service       mirrors    doc.security.allows(cap) + the per-record collab mirrors
3. Plugin capability      translates verbs → capabilities; gates; exposes twins
4. Chrome / host UI       renders    from the plugin's twins only
```

Each layer consumes only the layer below. A UI never touches scope strings or
`allows()` directly — what a verb requires is the owning plugin's knowledge
(search `'full'` needs `doc.text.search` **and** `doc.text.copy`; only the
search plugin should know that). Download and print have no plugin of their
own: their verbs live on the kernel's documents capability, and so do their
twins, `documents.canDownload()` and `documents.canPrint()`. How sharp a print
comes out (`doc.print.high`) isn't a yes or no: `documents.getPrintQuality()`
says it.

## The twin law

> **Every verb that can refuse ships a `can` twin: same name, same arguments,
> boolean, answering "would this verb succeed right now for this session?"**
>
> The twin composes everything the verb itself checks — authority (scope),
> document flags (`locked`, `lockedContents`, `/F` states), and structural
> preconditions (selection composition) — and excludes the active tool: twins
> are facts; mode gates whether affordances render at all, orthogonally.
> Verbs that cannot refuse get no twin.

Why twins and not a `permissions()` bag or a generic `can('verb')`:

- **Arity fidelity.** Questions have the same argument shape as their
  operations (`canDelete(ref)`, `canSearch(mode)`). A bag cannot hold
  argument-taking questions; twins cover every case with one rule.
- **Drift resistance.** The twin lives next to its verb and changes in the
  same diff; the 1:1 name/signature mapping admits a mechanical conformance
  test: _calling a gated verb while its twin is false must refuse._
- **Zero-judgment derivation.** The convention is a function, not a style
  guide: verb name → `can` prefix → same signature. Nothing to decide for
  any future plugin — which is how shape drift is prevented, not policed.

Bags may exist only as derived conveniences bundling twins for
one-subscription UIs (`comments.permissionsFor(ref)` is the precedent). They
never define answers of their own.

**The collapse rule.** Twins mirror answers, not method names: when every
verb in a family would compute the identical expression (one authority, no
flags, no argument variance), the family ships one twin named for the family
— `pageEdit.canEdit()` covers rotate/move/delete/insert/addBlank, because
`doc.pages.assemble` is their one shared answer and PDF itself has one
assemble bit. Two twins may never compute the same expression (two names for
one fact is drift surface); the moment an answer diverges — a capability
split, per-argument structure — the twin splits with it, in the same diff.

## Gate the optimistic, hide the rest

A client-side check inside the verb is required only where the verb creates
local/optimistic state or drives a gesture (annotation paint, form fill,
page reorder, redaction marks, render tile fetches): there, a late engine 403
leaves a lying UI or a zombie record, so the verb self-refuses at the
capability entry — gestures go inert; imperative calls reject with the same
`PermissionDenied` shape the engine throws. Pure-request verbs (search, text
read) need no client gate: hiding the affordance is the UX and the engine is
the enforcement — a duplicate check is drift surface.

Failed engine writes must still roll the optimistic state back. The gate makes
refusals rare (races, stale access); rollback keeps the model honest when they
happen anyway.

## Per-record authority (the ownership extension)

Where records carry owners (annotations via the collab grammar — future
entities slot into the same grammar), the doc-level booleans are not enough.
The owning plugin **projects this session's authority onto each record at
ingest** (`authority: { update, delete }`, asked of the security mirrors with
the record's stamped owner — the same inputs the server checks) and **fuses it
into the same predicate that already gates flags** (`annotTransformable`).
Presentation and behavior then agree by construction: a record you may not
edit renders the bare outline `locked` already renders — no handles, no knob,
no drag — and other people's records under an `annotations:update:self` grant
are selectable and commentable but immovable, per record, with no special
code.

Authority is model-owned derived state (like `apVersion`), session-stable, and
re-stamped when security state changes (owner unlock, `/access` refresh).

## Reactivity

Twins are pure reads, cheap enough to derive per call; UIs subscribe through
selectors (`useSelector(Token, c => c.canFoo())` — boolean equality is free).
Twins with structural inputs flip on selection changes, not just security
changes: subscribe, don't cache. A security-state change must tick the store
so every twin's subscribers re-render.

## Current twin registry (as built)

| Capability       | Twins                                                                                           | Composes from                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ---------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| annotation       | `canRead()` · `canCreate()` · `canEdit(ref)` · `canDelete(ref)` · `canGroup()` · `canUngroup()` | `doc.annotate.read` / collab mirrors + flags + structure                                                                                                                                                                                                                                                                                                                                                                            |
| comments         | `permissionsFor(ref)` (convenience bag of twins)                                                | the annotation twins                                                                                                                                                                                                                                                                                                                                                                                                                |
| selection        | `canSelect()` · `canCopy()`                                                                     | `doc.text.select` / `doc.text.copy`                                                                                                                                                                                                                                                                                                                                                                                                 |
| search           | `canSearch(mode?)`                                                                              | `doc.text.search`; `'full'` also `doc.text.copy` (a snippet reproduces text). No verb gate — pure request.                                                                                                                                                                                                                                                                                                                          |
| render           | `canRender()`                                                                                   | `doc.render`; gate case: `renderPage` and tile fetches refuse locally (else a denied viewport 403s per tile, forever)                                                                                                                                                                                                                                                                                                               |
| page-edit        | `canEdit()` (the collapse rule) · `canExtract()`                                                | `doc.pages.assemble` (rotate/move/delete/insert are one answer; PDF has one assemble bit); `doc.download` for extract and duplicate                                                                                                                                                                                                                                                                                                 |
| metadata         | `canUpdate()`                                                                                   | `doc.metadata.modify` (the standard fields and your own)                                                                                                                                                                                                                                                                                                                                                                            |
| outline          | `canEdit()`                                                                                     | `doc.outline.modify` (add, change, move and delete items; reading needs only `doc.open`)                                                                                                                                                                                                                                                                                                                                            |
| form             | `canRead()` · `canFill()` · `canDesign()`                                                       | `doc.forms.read` (hydration gate) / `doc.forms.fill` (fused into `FillItem.disabled` + write/reset gates) / `doc.forms.modify` (place/update/delete/detach; the draw-to-place handler's gate)                                                                                                                                                                                                                                       |
| redaction        | `canMark()` · `canApply()`                                                                      | marks are annotations ⇒ `canMark` is `annotation.canCreate()`; apply mirrors all three engine assertions: `doc.redact` ∧ `doc.pages.modify` ∧ `doc.annotate.modify` (+ engine support)                                                                                                                                                                                                                                              |
| kernel documents | `canDownload(id?)` · `canPrint(id?)`                                                            | `doc.download` / `doc.print`: the verbs (`download()`, `downloadLayer()`, `preparePrint()`, `print()`) live on the kernel's documents capability, so their twins do too. `canPrint` also needs the host's printer (`createKernel({ printer })`); `getPrintQuality(id?)` reads `doc.print.high` (the PDF copy with `doc.download` or `doc.download.flattened` and a printer that prints PDFs; else 300 DPI pictures; 150 without it) |
| kernel documents | `canOpenFiles()`                                                                                | the engine's own fact, `engine.canOpenFiles`: the local engine opens a file's bytes (or a URL the kernel downloads); the cloud engine only the documents your server holds                                                                                                                                                                                                                                                          |

---

### Conventions Plugins (`docs/conventions/plugins.md`)

# Plugins

How to write a plugin: its files, its manifest, its controller and
contracts, how it declares and reaches its dependencies, how it owns what it
acquires, and how it fails. Where a plugin fits in the system:
[`architecture.md`](./architecture.md). Its state and events:
[`state-and-sync.md`](./state-and-sync.md) and [`events.md`](./events.md). How
it answers "may this session do that?": [`permissions.md`](./permissions.md).

## Anatomy

Every plugin has the same layout, so any file can be found without searching:

```
packages/plugin/<name>/
  src/
    <name>.plugin.ts   the manifest
    token.ts           the capability token
    contract.ts        the public lens
    host-contract.ts   optional: the host lens
    internal.ts        optional: framework-only helpers
    index.ts           the implementation entry
    model.ts           state, transitions, projections, folds
    controller.ts      create(ctx) → { api, connect? }
    connect.ts         optional: wiring to sibling plugins
    services/          optional: plugin-private helpers shared by areas
    read/ write/ sync/ tools/   optional: one area per file
  test/                mirrors src/
```

| File               | Holds                                                                                            | Never holds                         |
| ------------------ | ------------------------------------------------------------------------------------------------ | ----------------------------------- |
| `<name>.plugin.ts` | `definePlugin(...)` inside the plugin factory                                                    | logic, subscriptions, registrations |
| `token.ts`         | the one `createCapabilityToken` call                                                             | anything else                       |
| `contract.ts`      | the capability interface, its public types, the token re-exported                                | implementation                      |
| `host-contract.ts` | the wider interface for siblings and adapters, and the same token widened with `createHostToken` | implementation                      |
| `internal.ts`      | helpers the framework adapters need and applications must not use                                | public API                          |
| `index.ts`         | the plugin factory and `export * from './contract'`                                              | anything else                       |
| `model.ts`         | the state type, `initial<Name>State()`, pure transitions, pure projections, folds                | I/O, engine calls, the context      |
| `controller.ts`    | `create<Name>Controller(ctx, config)`: builds the capability                                     | sibling registrations               |
| `connect.ts`       | `connect<Name>(ctx, api)`: registrations with siblings                                           | verbs, reads                        |

## The manifest

```ts
// packages/plugin/metadata/src/metadata.plugin.ts
export const metadataPlugin = () =>
  definePlugin({
    id: 'metadata',
    token: MetadataToken,
    scope: 'document',
    create: createMetadataController,
  });
```

`definePlugin(definition)` returns its argument, and reads the plugin's types
from it: the capability from `token`, the state from `state`, the settings from
`settings.defaults`. A manifest passes no type arguments. `create` receives a
`PluginContext<State, Settings>` and must return the capability; it is checked
against those types and never names them, so a controller that returns less
than the capability is a type error, not a narrower capability. A plugin
without `state` is stateless (`void`), and one without `settings` has
`NoSettings`: `ctx.settings()` throws there.

| Field      | Meaning                                                                                                                                                                                                                              |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`       | Unique in the plugin list. Names the state slice and appears in errors.                                                                                                                                                              |
| `token`    | The capability token this plugin provides. Omit for a plugin without a capability.                                                                                                                                                   |
| `scope`    | `'workspace'` (the default: one instance) or `'document'` (one instance per open document).                                                                                                                                          |
| `requires` | Tokens that must be installed. Validated when the kernel is created.                                                                                                                                                                 |
| `optional` | Tokens this plugin uses when they are installed.                                                                                                                                                                                     |
| `state`    | `() => State`: the initial session state, built for every instance. Omit for a stateless plugin.                                                                                                                                     |
| `settings` | `{ defaults, registered }`: the plugin's settings, what the app registered over the defaults. The kernel builds their store when it plans the plugin list; `ctx.settings()` reads it. See [Settings and state](#settings-and-state). |
| `create`   | `(ctx) => { api, connect? }`: builds one instance.                                                                                                                                                                                   |
| `inScope`  | Workspace plugins that take a document: `(api, documentId) => api`, the capability as seen from inside that document's scope, where calls that leave out the document use it. The kernel builds it once per document.                |

Configuration is an argument of the factory: its settings go into the
definition (see [Settings and state](#settings-and-state)), and anything else
is passed on to the controller through `create`:

```ts
// packages/plugin/form/src/form.plugin.ts
export const formPlugin = (config?: FormConfig) =>
  definePlugin({
    id: 'form',
    token: FormToken,
    scope: 'document',
    requires: [InteractionToken],
    optional: [AnnotationToken, ActionsToken],
    state: initialFormState,
    settings: { defaults: FORM_DEFAULTS, registered: config },
    create: createFormController,
  });
```

A definition is an immutable recipe that two kernels may share. Nothing that
belongs to an instance (registries, caches, timers) lives in the manifest or
in module scope; it lives in the controller.

## Tokens and contracts

**The token.** `token.ts` calls `createCapabilityToken` once for the package.
The `hint` is appended to the error a user sees when a plugin requires this
token and it is not installed, so the error carries its own fix.

```ts
// packages/plugin/form/src/token.ts
export const FormToken = createCapabilityToken<FormCapability>('form', {
  hint: `add formPlugin() from '@embedpdf/plugin-form' to your plugins list`,
});
```

**The public lens.** `contract.ts` declares the capability interface
(`<Name>Capability`) and every type it mentions, and re-exports the token.
Application code, other plugins and adapters depend on this file. Every member
gets TSDoc as described in [`comments.md`](./comments.md): a one-sentence
summary, the `PluginError` codes it rejects with, and the events it fires.

**The host lens.** Some members exist only for sibling plugins and framework
adapters: registration seams, render feeds, sinks. They go into
`<Name>HostCapability extends <Name>Capability` in `host-contract.ts`, typed
over the same runtime token:

```ts
// packages/plugin/form/src/host-contract.ts
export interface FormHostCapability extends FormCapability {
  ensureLoaded(page: PageRef): Promise<void>;
  notifyWidgetEvent(
    field: FormFieldRef,
    widget: AnnotationRef,
    event: PdfAnnotationEventKind,
  ): void;
  // …
}

export const FormToken = createHostToken<FormHostCapability>(PublicFormToken);
```

`createHostToken` returns the same object typed wider, so both lenses resolve
the same instance. A plugin with a host lens registers the host token in its
manifest, so `definePlugin` reads the host capability from it.

**Framework-only helpers.** `internal.ts` holds what adapters need and
applications must not use. It is a visibility boundary, not a bundle
boundary.

## Package entries and bundle boundaries

Every plugin package publishes deliberate dependency doors:

- `@embedpdf/plugin-<name>` is the implementation opt-in: import it for
  `<name>Plugin()`, or from the framework feature entry that installs and
  re-exports that plugin. The root re-exports the public contract.
- `@embedpdf/plugin-<name>/contract` is the public capability protocol: the
  one runtime token, the types, and small protocol helpers. Its runtime
  import graph must not reach the implementation (the manifest or the
  controller).
- `@embedpdf/plugin-<name>/contract/host` is optional. It exposes the host
  lens over the same token object.
- Named entries such as `/destination`, `/authoring` or `/scripting` expose a
  deliberate pure helper feature that is neither a contract nor the full
  implementation.

The source rules are mechanical:

1. Plugin source never imports another plugin's bare package root, not even
   for types. Use `/contract`, `/contract/host`, or a named helper entry.
2. Framework source follows the same rule for sibling plugins. A framework
   feature may import its own plugin's root only when that feature entry
   re-exports the root and therefore opts users into the implementation.
3. Application and composition source may import a bare plugin root in a
   module that imports that plugin's factory. Elsewhere, even type-only and
   token imports use the contract entry.
4. `requires` and `optional` describe kernel relationships. They do not
   create a JavaScript module boundary and do not replace contract imports.
5. The token is defined once. The root, `/contract`, `/contract/host` and
   `/internal` re-export or widen the same token; `createCapabilityToken` is
   never called twice.

`pnpm check:plugin-boundaries` checks these rules locally and in CI.

## The controller

`controller.ts` exports `create<Name>Controller(ctx, config?)`, the manifest's
`create`. It returns `{ api, connect? }`:

- `api` is the capability. It is built once and never replaced.
- `connect` runs once, after this plugin and its dependencies are created (see
  [`architecture.md`](./architecture.md#scopes-and-bring-up)). The first loads
  of the plugin's mirrors start right after it.

Inside `create` a controller creates its event sources, its mirrors and its
`ctx.state.onChange` listener, and assembles the reads and verbs. It may
resolve siblings it needs for its reads and verbs. It does not register
anything with a sibling, and does not subscribe to sibling events or to
document events (a mirror subscribes by itself); that is `connect`'s job.

A controller that grows past a few hundred lines splits into areas, one per
file under `read/`, `write/`, `sync/` and `tools/`. Each area is a function
that receives the context and the services it needs, and returns its slice of
the capability with `satisfies Partial<Capability>`. `composeApi` joins the
slices and throws if two slices define the same member:

```ts
// packages/plugin/form/src/controller.ts
export function createFormController(ctx: FormContext) {
  const services = createServices(ctx);
  const { events } = services;

  const fields = createFieldReads(ctx, services);
  const widgets = createWidgetReads(ctx, services);
  const values = createValueWrites(ctx, services);
  // …

  const api = composeApi('form', [
    ctx.settings().api,
    fields.api,
    widgets.api,
    values.api,
    // …
    {
      canFill: () => ctx.allows('doc.forms.fill'),
      onValueChanged: events.valueChanged.on,
      // …
    },
  ]) satisfies FormHostCapability;

  return {
    api,
    connect: () => connectForm(ctx, api),
  };
}
```

`services/` holds what several areas share, built once by
`createServices(ctx)`: the mirrors, the event sources, resolved siblings,
queues. A plugin that uses the context in many files names its type once:
`export type FormContext = PluginContext<FormState, FormSettings>` in
`services/context.ts`.

## `connect.ts`

Everything that registers with a sibling runs in `connect`: tools and
handlers on the interaction hub, behaviors on the annotation plugin, executors
and sinks on the actions plugin, subscriptions to sibling events, reactions
to document events, `ctx.watch` on state the plugin does not own. Every
registration's `Unsubscribe` goes to `ctx.cleanup`, even when both sides
close together.

```ts
// packages/plugin/link/src/connect.ts
export function connectLink(ctx: PluginContext<void>): void {
  const annotation = ctx.tryGet(AnnotationToken);
  if (!annotation) return;
  const interaction = ctx.get(InteractionToken);
  ctx.cleanup(
    annotation.registerBehavior({
      id: 'link-nav',
      matches: (target) => target.subtype === 'link',
      engaged: () => interaction.activeToolEnables('link-nav'),
    }),
  );
}
```

## Dependencies

| Member                                  | Behavior                                                                                                 |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `ctx.get(token)`                        | The capability for this plugin's scope (this document, or the workspace). Throws when it cannot resolve. |
| `ctx.tryGet(token)`                     | The same, or `null` when the plugin is not installed or not available.                                   |
| `ctx.forDocument(token, documentId)`    | The capability for another document. For workspace plugins.                                              |
| `ctx.tryForDocument(token, documentId)` | The same, or `null`.                                                                                     |

- A plugin declares every token it resolves: required ones in `requires`,
  optional ones in `optional`. Its own token and `DocumentsToken` need no
  declaration. The host token of a plugin is the same object as its public
  token, so declaring either covers both.
- In development (`NODE_ENV` is not `production`), resolving an undeclared
  token throws:

  ```
  [kernel] plugin "x" resolved capability "y" without declaring it; add the token to its `requires` or `optional` list.
  ```

- The one exception is a plugin that resolves capabilities for code the host
  supplies, such as the commands plugin running command definitions. It sets
  `resolvesAnyCapability: true` in its manifest; the kernel then allows any
  token and neither orders nor validates those dependencies.

- `createKernel` validates the plugin list before building anything, and
  rejects: the same plugin installed twice, two plugins with one id, two
  plugins providing one token, a required token no plugin provides (with the
  token's `hint`), a workspace plugin that requires a document-scoped token,
  and dependency cycles.
- Plugins are created and connected in dependency order, so a required
  sibling exists when `create` runs.
- A workspace plugin reaches document plugins with `ctx.forDocument` and
  declares their tokens as `optional`.
- A workspace capability resolved for a document (`ctx.get` in a document
  plugin, `useCommands()` inside a `<DocumentScope>`) is its `inScope` view:
  `commands.execute(id)` runs for that document, `documents.download()`
  downloads it.

## The document

- `ctx.doc` is the guarded engine handle of the plugin's document. Every call
  through it rejects `instance-closed` once the instance closed, is aborted
  when the instance closes, and rejects with `PluginError` instead of raw
  engine errors. It is never null in a document plugin, so it is never
  null-checked. Reading it in a workspace plugin throws.
- `ctx.document()` is the page registry (`DocumentMeta`: pages, revision,
  render policy). A page argument is a `PageRef` or an index, looked up two
  ways. Reads use `ctx.getPage(page)`, which returns the page or `null`; verbs
  use `ctx.pageOf(page)`, which returns the page or throws `not-found`.
  `ctx.assertPageRef(ref)` throws `not-found` for a foreign page.
- A read given a page that no longer exists returns empty (`[]`, `0` or
  `null`); a verb refuses it. Reads run while rendering, and a page deleted
  mid-render must not break a layer: Search's `listHits({ page })` and
  `getHitCount(page)` answer `[]` and `0` for it, and `search({ from })`
  rejects `not-found`.
- `ctx.allows(permission)` is what every `can*` twin reads, and
  `ctx.assertAllowed(permission, operation)` is how the verb refuses: it
  throws `permission-denied` with `error.permission` set. Both read the bound
  document, so a workspace plugin asks the `can*` twin of the document plugin
  it acts through (`ctx.forDocument(token, documentId)`).
- Engine values are in page space, the space plugins work in, so nothing
  converts them.

## Lifetime and async

Everything a plugin acquires is owned by its instance and released when the
instance closes (a document plugin) or the kernel is destroyed (a workspace
plugin).

| Member                          | Behavior                                                                                                                                                                                                                                                                                                                                                          |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ctx.cleanup(teardown)`         | Run `teardown` at close. Asynchronous teardowns are awaited. Registering after the owner closed runs the teardown immediately.                                                                                                                                                                                                                                    |
| `ctx.listen(source, listener)`  | Subscribe to an `EventHook` or anything with `subscribe` for the instance's lifetime. The kernel owns the unsubscribe.                                                                                                                                                                                                                                            |
| `ctx.acquire(get, dispose)`     | `get(lifetime)` a resource and register `dispose`. A resource that arrives after close is disposed and the call rejects `instance-closed`.                                                                                                                                                                                                                        |
| `ctx.latest(key)`               | A newest-wins lane: `lane.run(async (run) => …, options)`. Starting a run aborts the previous one; a superseded run cannot publish (`run.commit(fn)` returns false) and rejects `operation-cancelled`.                                                                                                                                                            |
| `ctx.serialQueue(key?)`         | A per-key queue: operations run one at a time, in submission order, and a failure does not affect later operations. `queue.idle()` resolves once everything queued so far has finished. `queue(operation, { signal })` skips an operation whose signal fired before it started.                                                                                   |
| `ctx.onSettle(flush)`           | Run `flush` before the document's file is read, and wait for it: send what the plugin holds back from the engine. Document plugins only. See [`state-and-sync.md`](./state-and-sync.md#held-back-writes).                                                                                                                                                         |
| `ctx.aroundDownload(wrap)`      | Run something of the document's own around every read of its file: `wrap(read)` returns the bytes `read()` gives, doing what comes before and after (the actions plugin's save actions). Document plugins only; the first registered is outermost.                                                                                                                |
| `ctx.aroundPrint(wrap)`         | Run something of the document's own around every print dialog: `wrap(print)` calls `print()`, doing what comes before and after (the actions plugin's print actions). A print that starts inside the wrap (a document script's `print()`) is started, never awaited: awaiting it would wait for itself. Document plugins only; the first registered is outermost. |
| `ctx.cancellable(signal, task)` | Run an engine call the caller can cancel: it is aborted when `signal` fires, and rejects `operation-cancelled`. Every async verb passes its `options.signal` through it.                                                                                                                                                                                          |
| `ctx.events.source<T>()`        | An event source, disposed at close.                                                                                                                                                                                                                                                                                                                               |
| `ctx.clock`                     | Time, from the host: `after(ms, run)` and `nextFrame(run)` return their cancel; `hasFrames` is false where nothing paints (Node, most tests). What the instance scheduled is cancelled at close.                                                                                                                                                                  |

- Queues come from `ctx.serialQueue(key)`, never from a local
  `createSerialQueue()`.
- Time comes from `ctx.clock`, never from the environment: no `setTimeout`,
  `setInterval` or `requestAnimationFrame` in a plugin or a `*-core` package
  (lint enforces it). The host decides what a timer and a frame are when it
  builds the kernel (`createKernel({ clock })`): the framework viewers pass
  `browserClock()` from `@embedpdf/web`, and without one the kernel uses
  `timerClock`, timers and no frames. A helper an app calls with no context,
  such as `persistStampLibraries`, coalesces with promise steps instead of a
  timer.
- An operation on queue A may enqueue into queue B and await it, but never
  the other way round on the same pair: that deadlocks.
- The search plugin runs every search on `ctx.latest('session')`, so a new
  query cancels the previous scan and a stale scan can never write its hits.

## Settings and state

Every plugin with settings declares them in its definition, and declares the
state a UI shows once, next to its contract.

```ts
// search.plugin.ts: the settings, what the app registered over the defaults.
// SearchConfig is DeepPartial<SearchSettings>. No type arguments: the settings
// type comes from SEARCH_DEFAULTS, the state from initialSearchState, the
// capability from SearchToken.
export const searchPlugin = (config?: SearchConfig) =>
  definePlugin({
    id: 'search',
    token: SearchToken,
    // …
    state: initialSearchState,
    settings: { defaults: SEARCH_DEFAULTS, registered: config },
    create: createSearchController,
  });

// controller.ts: the context type is the controller's parameter, and the
// definition is checked against it. The capability spreads the settings calls;
// the controller reads get() where it uses a setting, so a change is live.
export function createSearchController(
  ctx: PluginContext<SearchState, SearchSettings>,
) {
  const settings = ctx.settings();
  // …
  const arrival = { ...settings.get().reveal, ...options };
  // …
  return {
    api: {
      ...settings.api /* getSettings, updateSettings, resetSettings, onSettingsChanged */,
      search,
    },
  };
}

// state.ts: the page's State table, as code
export const searchState = defineState(SearchToken, {
  read: (search) => ({
    hitCount: search.getHitCount(),
    status: search.getStatus(),
  }),
  empty: { hitCount: 0, status: 'idle' }, // with no document
});
```

- There is one settings store per plugin registration. The kernel builds it
  when it plans the plugin list, so the settings exist before any document
  opens: `kernel.settingsOf(token)` reads and changes them with no document,
  and an app may set them before opening one. Every open document shares the
  store, so `updateSettings()` reaches them all, and the ones opened later.
- Nested objects merge, arrays and values replace, `undefined` is ignored, and
  `resetSettings()` goes back to what the app registered. `onSettingsChanged`
  carries `{ settings, changed }`, `changed` naming the top-level settings
  whose value changed.
- A controller reads `settings.get()` where it uses a setting, never a copy
  made in `create`, so a change applies at once. The Stage keeps its own
  per-view settings.
- A controller names its context type in its parameter,
  `PluginContext<State, Settings>`, and the definition must match it: a
  definition that forgets `settings` while the controller reads them is a
  type error, and so is `ctx.settings()` in a plugin that declares none.
- Every value in the State table is state, and its event is a state-change
  event derived in the plugin's one `ctx.state.onChange` listener, even when
  it reads like progress: Search's `onProgressChanged` fires when `progress`
  changes, including its reset when a new search starts
  ([`events.md`](./events.md#state-change-events)).
- An adapter turns a state declaration into its framework's form with one
  generic function, and a plugin's settings with another (React:
  `export const useSearchState = stateHook(searchState)` and
  `export const useSearchSettings = settingsHook(SearchToken)`). Getters in
  `read` return the same value while nothing changed, so readers re-render
  only when a field does. The settings hook reads through the kernel, so it
  works without a document, and so do the settings calls of `useSearch()`'s
  stand-in.
- A color setting whose default follows the accent defaults to `null` in the
  plugin's defaults (selection's `color`, `handles.color`): the adapter paints
  it with `paint()` and, for a translucent one, `mixAccent()` from
  `@embedpdf/web`, so `--epdf-accent` and the viewer's `accent` reach it. A
  color with its own default (Search's `highlight.color`) is a plain value.
- The docs' Settings and State tables are checked against these declarations.

## Errors

A capability rejects (or throws) with `PluginError` only:

```ts
new PluginError(code, capability, message, { cause?, details?, permission? });
```

| Code                  | Meaning                                                                       |
| --------------------- | ----------------------------------------------------------------------------- |
| `permission-denied`   | The session may not do this. `error.permission` names the missing permission. |
| `unsupported`         | The engine or document cannot do this.                                        |
| `not-found`           | A ref names nothing in this document.                                         |
| `not-ready`           | The resource is not loaded or the document not open yet.                      |
| `invalid-input`       | The arguments are wrong.                                                      |
| `conflict`            | A concurrent change or a version conflict.                                    |
| `instance-closed`     | The instance closed before the call finished.                                 |
| `operation-cancelled` | The caller cancelled, or a newer call superseded this one.                    |
| `operation-failed`    | Anything else.                                                                |

- Calls through `ctx.doc` already reject with `PluginError`: the guarded
  handle maps engine errors with `toPluginError(capability, error)`.
  Cancellation becomes `operation-cancelled`, never `operation-failed`.
- Code that catches something else converts it with `toPluginError`.
- State and event payloads carry the serializable summary
  `toPluginErrorInfo(error)`, never the error object.
- Callers match on `code` (`isPluginError(error, 'conflict')`), never on the
  message.
- A verb that can refuse has a `can*` twin reading `ctx.allows(permission)`,
  and refuses with `ctx.assertAllowed(permission, operation)`, never a
  hand-built `PluginError` ([`permissions.md`](./permissions.md)).
- A refusal before anything started only rejects: no state changes and no
  event fires, not even the verb's failure event. Search refuses a search
  without `doc.text.search`, or from a page that isn't there, before its
  session is touched, so `getStatus()` is what it was and `onFailed` stays
  quiet. `onFailed` is for work that started and then failed.
- A best-effort batch verb resolves with `BatchResult<T, R>`
  (`applied`, `skipped`, `failed`) instead of rejecting on the first failure.

## A minimal complete plugin

A document-scoped plugin that mirrors the document title and remembers
whether a title editor is open. It shows a mirror with a fold, fact events
with their origin, `onResynced`, session state with transitions and a derived
state-change event, a verb with a permission gate, and the manifest.

```ts
// token.ts
import { createCapabilityToken } from '@embedpdf/core';

import type { TitleCapability } from './contract';

export const TitleToken = createCapabilityToken<TitleCapability>('title', {
  hint: "add titlePlugin() from '@embedpdf/plugin-title' to your plugins list",
});
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```ts
// model.ts
import type { DocumentEvent } from '@embedpdf/core';

/** The session state: whether the title editor is open. */
export interface TitleState {
  readonly editorOpen: boolean;
}

export const initialTitleState = (): TitleState => ({ editorOpen: false });

export const openEditor = (state: TitleState): TitleState =>
  state.editorOpen ? state : { ...state, editorOpen: true };

export const closeEditor = (state: TitleState): TitleState =>
  state.editorOpen ? { ...state, editorOpen: false } : state;

/** Apply one confirmed event to the mirrored title. The same for every origin. */
export const foldTitle = (
  title: string | null,
  event: DocumentEvent,
): string | null =>
  event.type === 'metadata.updated' ? event.metadata.title : title;
```

```
/* Detailed source-code truncated for AI context efficiency. */
```

```ts
// title.plugin.ts
import { definePlugin } from '@embedpdf/core';

import { TitleToken } from './contract';
import { createTitleController } from './controller';
import { initialTitleState } from './model';

/** The document title, mirrored from the engine, and whether its editor is open. */
export const titlePlugin = () =>
  definePlugin({
    id: 'title',
    token: TitleToken,
    scope: 'document',
    state: initialTitleState,
    create: createTitleController,
  });
```

```ts
// index.ts
export { titlePlugin } from './title.plugin';
export * from './contract';
```

Its tests are in [`testing.md`](./testing.md#the-test-context).

## Checklist

- The manifest only calls `definePlugin`, with no type arguments; settings
  are declared in it (`settings: { defaults, registered: config }`), and other
  configuration reaches the controller through `create`.
- One `createCapabilityToken` call, in `token.ts`; a host lens uses
  `createHostToken`.
- Every token the plugin resolves is in `requires` or `optional`, and is
  imported from a `/contract` entry.
- Session state changes only through transitions in `model.ts`.
- Engine data lives in a mirror or page mirror; verbs never write it.
- Events come from `ctx.events.source()`; fact events from where the
  confirmed event is applied, state-change events from `ctx.state.onChange`.
  A value in the State table is announced only from there.
- A getter given a page that isn't in the document returns empty; a verb
  refuses it with `not-found` (`ctx.getPage` for reads, `ctx.pageOf` for
  verbs).
- A refusal before the work starts only rejects: no state change, no event.
- Sibling registrations and subscriptions are in `connect`, and every
  `Unsubscribe` goes to `ctx.cleanup`.
- Every rejection is a `PluginError`, and the contract's TSDoc lists its codes.
- Tests under `test/` mirror `src/` (see [`testing.md`](./testing.md)).

---

### Conventions Runtime Confinement (`docs/conventions/runtime-confinement.md`)

# Thread-confined PDFium runtime

This server can render PDFs on **N worker threads in a single process**, in
parallel, with zero-copy `postMessage` transfer. This is not "thread-safe
PDFium" (upstream is explicit that its APIs are not thread-safe). Instead the
native runtime is built so that **each worker thread owns its own PDFium state**
and never shares PDFium handles across threads — a _thread-confined_ (shared-
nothing) contract under which N workers run safely in parallel.

## The contract

1. Each worker thread calls `EPDF_InitThread` before using PDFium and
   `EPDF_ShutdownThread` after closing every handle it created
   (wired in `packages/engine/services/src/runtime/lifecycle/bootstrap.ts`).
2. No document, page, bitmap, annotation, text page, content parser, or font
   object may cross workers.
3. The pool enforces this: a doc is pinned to one worker via sticky-by-docId
   routing in `cloudpdf/server/src/runtime/WorkerThreadPool.ts`.

Two things are private-per-thread, which together make parallel rendering safe:
the **handles** we create (pinned per worker) and PDFium's internal **globals**
(made `thread_local` in the native build).

## How it's built (per-target opt-in)

Per-thread globals are gated behind a real GN arg, **default off**:

- Declared in `packages/engine/runtime/runtime-src/pdfium.gni`
  (`embedpdf_thread_local_globals = false`).
- Translated to the `EPDF_THREAD_LOCAL_GLOBALS` define in
  `packages/engine/runtime/runtime-src/BUILD.gn`, consumed by the `EPDF_TLS` macro
  (`core/fxcrt/epdf_tls.h`).
- Turned **on per native target** (and left **off for wasm**) by
  `packages/engine/runtime/runtime-src/scripts/embedpdf-runtime/build-target.sh`.
  wasm needs nothing — each instance already isolates globals via its own
  linear memory.

Flag-on is a superset: the build still runs correctly single-threaded. Default
off keeps every other target and upstream rebases byte-for-byte unchanged.

## Configuring the worker pool

Worker count resolves as: explicit `size` option → `CLOUDPDF_WORKER_POOL_SIZE`
env → conservative default `min(2, cpus)`.

```bash
# one worker per CPU (after the gate below is green for your native build)
CLOUDPDF_WORKER_POOL_SIZE=max   # or an explicit integer, e.g. 8
```

Keep the conservative default until you have validated the thread-confined
native build with the gate below.

## The gate (run before scaling in production)

Fix-then-verify order — the two known addon/runtime data races are already fixed
(`g_napi_lossless` made a per-call local; default `localtime` made thread-safe),
so a TSAN run surfaces PDFium findings rather than masking them.

1. **Threaded soak** (`testing/tools:epdf_thread_soak`) — multi-thread
   init/open/render/**encrypted-save**/close. The encrypted-save path exercises
   the now doc-owned pending-security state across threads.

   ```bash
   # plain build, native target (defaults EMBEDPDF_TLS_GLOBALS=true)
   packages/engine/runtime/runtime-src/scripts/embedpdf-runtime/thread-soak-target.sh \
     darwin-arm64 path/to/icc-and-font-heavy.pdf -- --threads=8 --iterations=200

   # under ThreadSanitizer (the gate)
   EMBEDPDF_TSAN=1 \
   packages/engine/runtime/runtime-src/scripts/embedpdf-runtime/thread-soak-target.sh \
     linux-x64 path/to/icc-and-font-heavy.pdf -- --threads=8 --iterations=200
   ```

   Use ICC-heavy PDFs to cover the LCMS default-context path (see the gate note
   in `packages/engine/runtime/runtime-src/core/fxcodec/icc/icc_transform.cpp`).

2. **Single-thread regression** — run the embeddertests to confirm the
   `thread_local` variant is still correct single-threaded:

   ```bash
   packages/engine/runtime/runtime-src/scripts/embedpdf-runtime/test-target.sh linux-x64
   # baseline (process-global) comparison:
   EMBEDPDF_TLS_GLOBALS=false \
     packages/engine/runtime/runtime-src/scripts/embedpdf-runtime/test-target.sh linux-x64
   ```

Only after TSAN + soak are green should you raise the pool size in production.

## Load-test methodology

Compare throughput/latency across three configurations on representative
(ICC-heavy, font-heavy, encrypted-save) corpora:

- `CLOUDPDF_WORKER_POOL_SIZE=1` — single worker baseline.
- `CLOUDPDF_WORKER_POOL_SIZE=max` — one thread-confined worker per CPU.
- A process-pool deployment (multiple single-thread pods/processes) — the
  crash-isolated comparison point.

Track p50/p95/p99 latency, sustained throughput, and **RSS** (per-thread memory
matters — see below).

## Tradeoffs

- **Per-thread memory:** each thread duplicates the GEModule (FreeType + font
  caches), CMaps, stock colorspaces, and the opcode table. N threads ≈ N× that
  font/cmap footprint. Size the pool (and pod memory) accordingly.
- **Thread confinement is not crash isolation:** a malformed-PDF segfault kills
  the process that owns the worker threads. Inline mode therefore loses the API
  process and all workers. Host mode moves the worker pool into supervised child
  processes, but all workers within one host still share its crash domain. See
  [`engine-host-isolation.md`](./engine-host-isolation.md).
- Keep `pdf_use_partition_alloc=false` (or make `g_allocators` thread_local if
  it is ever re-enabled).

---

### Conventions Server Adapters (`docs/conventions/server-adapters.md`)

# Server Adapter House Style

The server has five adapter families: **Secrets**, **KMS**, **Storage**, **CDN**, **Import** (read-only pull sources for `documents.importFrom`). They differ in _what they do_ but share _how they're structured_. This document is the contract a new adapter must follow.

Unless stated otherwise, adapter source paths below are relative to
`cloudpdf/server/src/`; test paths are relative to `cloudpdf/server/`.

---

## File layout

Every family lives at the same shape:

```
<family>/
├── <Family>.ts                     interface (role-noun, includes `info`)
├── create<Family>.ts               factory function (switch on kind)
├── config/
│   ├── <Family>ConfigSchema.ts     Zod discriminated union over `kind`
│   └── load<Family>ConfigFromEnv.ts
└── adapters/
    ├── <Kind1><Family>.ts          one file per provider
    └── <Kind2><Family>.ts
```

Examples: `cloudpdf/server/src/security/kms/adapters/AwsKmsKeyring.ts`,
`cloudpdf/server/src/storage/adapters/S3ObjectStore.ts`, and
`cloudpdf/server/src/cdn/adapters/BunnyCdnSigner.ts`.

---

## Interface convention

```typescript
interface <Family> {
  readonly info: { kind: <FamilyKind>; ...public_diagnostic_fields };
  // family-specific methods
}
```

Rules:

- `kind` is the discriminator and matches the config Zod schema.
- Other `info` fields are **public identifiers only** — bucket names, distribution domains, key IDs, hostnames. **Never secrets.**
- Decorators (caching wrappers, etc.) add fields rather than mutating `kind`. Example: `info: { kind: 'aws-sm', cached: true }`, not `kind: 'aws-sm+cache'`.

---

## Lazy-load house pattern for cloud-SDK adapters

Every cloud-SDK adapter follows this exact shape. The pattern is non-negotiable so installs don't need every cloud SDK and so server boot stays fast.

### Canonical form

```typescript
import type { /* type-only imports for SDK shapes */ } from '@scope/package';
// NB: type-only imports don't trigger the lazy load — only `await import(...)` does.

type Module = typeof import('@scope/package');
type Client = InstanceType<Module['Client']>;

export class XxxAdapter implements YyyInterface {
  readonly info = { kind: 'xxx' as const, /* public diagnostics */ };

  // Promise-typed field — stored, not awaited at construction
  private readonly clientPromise: Promise<{ client: Client; /* commands */ }>;

  constructor(private readonly opts: XxxConfig) {
    this.clientPromise = this.createClient();
  }

  private async createClient(): Promise<{ client: Client; /* commands */ }> {
    const mod = (await import('@scope/package')) as Module;
    return {
      client: new mod.Client(/* ... */),
      // Capture command constructors here too — the SDK uses
      // command objects you instantiate per call.
    };
  }

  async someOperation(...) {
    const { client, /* commands */ } = await this.clientPromise;
    return client.send(new commands.SomeCommand(/* ... */));
  }
}
```

### Why this exact shape

| Decision                                                                            | Why                                                                                                                             |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `await import('@scope/package')` in a private async method                          | Dynamic import doesn't run at module load. The SDK package never resolves unless an instance is constructed.                    |
| `clientPromise` stored at construction (`this.clientPromise = this.createClient()`) | Single shared promise. First operation awaits it; subsequent operations reuse the same resolved client. No reconnection storms. |
| **Stored, not awaited** at construction                                             | Constructor stays synchronous. `new XxxAdapter(opts)` returns immediately. The connection happens on first use.                 |
| Command constructors returned alongside the client                                  | AWS SDK v3 / GCP / Azure use command objects. Capturing them in `createClient` avoids per-call `await import` calls.            |
| Type-only imports for SDK shapes                                                    | TypeScript can infer types from the SDK without forcing a runtime require. Pair `import type` with `typeof import(...)`.        |

### Azure variant — parallel imports

Azure adapters typically need two packages (`@azure/identity` for auth + a service SDK). Parallelize the imports:

```typescript
private async createClient(): Promise<Client> {
  const [identity, keys] = await Promise.all([
    import('@azure/identity') as Promise<typeof import('@azure/identity')>,
    import('@azure/keyvault-keys') as Promise<typeof import('@azure/keyvault-keys')>,
  ]);
  return new keys.CryptographyClient(this.keyId, new identity.DefaultAzureCredential());
}
```

### `optionalDependencies`

Every cloud-SDK package **must** be listed in `cloudpdf/server/package.json`
under `optionalDependencies`, never `dependencies`. This way:

- `npm install` doesn't pull SDKs the user isn't using.
- Errors at adapter construction time are clear: "Cannot find module '@scope/package' — install it to use the X adapter."
- CI can install with `--no-optional` to verify the core server works without any cloud SDK.

Current cloud-SDK adapters using the pattern (all match exactly):

| Adapter                        | SDK package                                   |
| ------------------------------ | --------------------------------------------- |
| `S3ObjectStore`                | `@aws-sdk/client-s3` + `s3-request-presigner` |
| `AwsKmsKeyring`                | `@aws-sdk/client-kms`                         |
| `GcpKmsKeyring`                | `@google-cloud/kms`                           |
| `AzureKeyVaultKeyring`         | `@azure/identity` + `@azure/keyvault-keys`    |
| `AwsSecretsManagerProvider`    | `@aws-sdk/client-secrets-manager`             |
| `GcpSecretManagerProvider`     | `@google-cloud/secret-manager`                |
| `AzureKeyVaultSecretsProvider` | `@azure/identity` + `@azure/keyvault-secrets` |
| `GcsObjectStore`               | `@google-cloud/storage`                       |
| `AzureBlobObjectStore`         | `@azure/storage-blob` + `@azure/identity`     |

Any new cloud-SDK adapter (CDN CloudFront / CloudCDN / AzureFD purge) follows the same shape.

---

## Config schema convention

`<family>/config/<Family>ConfigSchema.ts`:

```typescript
export const <Family>ConfigSchema = z.discriminatedUnion('kind', [
  z.object({ kind: z.literal('a'), ...fields }),
  z.object({ kind: z.literal('b'), ...fields }),
]);
export type <Family>Config = z.infer<typeof <Family>ConfigSchema>;
```

Fields that may hold secrets: `z.union([SecretRefSchema, z.string()])`. The string form is for local-dev convenience (literal value); the `SecretRef` form is for production. The factory's resolver injection (see below) handles both.

---

## Factory convention

`<family>/create<Family>.ts`:

```typescript
export async function create<Family>(
  config: <Family>Config,
  opts: { resolver?: SecretResolver } = {},
): Promise<<Family>> {
  switch (config.kind) {
    case 'a': return new A<Family>(config, opts);
    case 'b': return new B<Family>(config, opts);
    // exhaustive — TypeScript enforces no missing cases
  }
}
```

The `{ resolver }` opt is the shared mechanism for resolving `SecretRef` fields in config. Each adapter that needs it pulls the resolver out of opts and resolves at construction (or lazily, depending on the adapter).

---

## Env-var loader convention

`<family>/config/load<Family>ConfigFromEnv.ts`:

```typescript
export function load<Family>ConfigFromEnv(env: NodeJS.ProcessEnv): <Family>Config {
  const kind = env['CLOUDPDF_<FAMILY>_KIND'] ?? '<default>';
  const raw = { kind, ...kindSpecificFields(env, kind) };
  return <Family>ConfigSchema.parse(raw); // throws with helpful message
}
```

Env-var conventions:

- `CLOUDPDF_<FAMILY>_KIND` — selector
- `CLOUDPDF_<FAMILY>_<KIND>_<FIELD>` — variant-specific fields
- Secret values accept the `secret://<provider>/<name>?jsonKey=...&encoding=...` URI form (parsed to `SecretRef`) or plain strings (used literally)

---

## Redaction

When a config is logged or surfaced through an admin endpoint, run it through `redactConfig` from `config/secrets/redact.ts`:

```typescript
import { redactConfig } from '../../config/secrets/redact';

logger.info({ config: redactConfig(myConfig) }, 'starting');
```

- `SecretRef`-shaped values become `<SecretRef provider/name>`
- Caller can pass `additionalSensitiveKeys` to redact literal-string secret values by field name

A test (`cloudpdf/server/test/config-redact.test.ts`) pins that secret material
never leaks through `JSON.stringify` of a redacted config.

---

## The bootstrap recipe

In `cloudpdf/server/src/bin/cloudpdf-server.ts`:

```typescript
// 1. Secrets — primary user-facing utility.
//    Registry takes the full SecretsConfig and applies
//    CachingSecretsProvider automatically when config.cache is set.
const secrets = createSecretsProviderRegistry(loadSecretsConfigFromEnv(env));
const resolver = createSecretResolver(secrets);

// 2. Each family loads its config, takes the resolver
const [kms, objectStore, cdnSigner] = await Promise.all([
  createKmsKeyring(loadKmsConfigFromEnv(env), { resolver }),
  createObjectStore(loadObjectStoreConfigFromEnv(env), { resolver }),
  createCdnSigner(loadCdnConfigFromEnv(env), { resolver }),
]);

// 3. buildApp — Secrets is not here, stays in scope for user code
await buildApp({ kms, objectStore, cdnSigner /* ... */ });
```

No `createServerRuntime` or other bundler. Five lines of factory composition is the standard, and it keeps `secrets` and `resolver` first-class for user code that needs to fetch its own secrets (JWT signing keys, DB credentials, third-party integration tokens).

### Caching control

`createSecretsProviderRegistry(config)` applies `CachingSecretsProvider` to every provider when `config.cache` is present. Three tiers of control:

| Goal                                                      | How                                                                                                                                                            |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Default deployment (caching on)                           | `config.cache: { ttlSec: 3600 }` — `loadSecretsConfigFromEnv` sets this by default                                                                             |
| Custom TTL                                                | `config.cache: { ttlSec: 300 }` (or `CLOUDPDF_SECRETS_CACHE_TTL_SEC=300`)                                                                                      |
| No caching                                                | Omit `cache` from the programmatic config; or `CLOUDPDF_SECRETS_CACHE_TTL_SEC=0`                                                                               |
| Per-provider caching, custom decorators, mocked providers | Skip the registry helper — build providers manually via `createSecretsProvider`, compose into a `Map<string, SecretsProvider>`, pass to `createSecretResolver` |

---

## Adding a new adapter — checklist

1. **Interface** — already exists for the family; you don't touch it.
2. **Zod schema variant** — add one entry to `<family>/config/<Family>ConfigSchema.ts`'s discriminated union.
3. **Adapter class** — one file in `<family>/adapters/<Kind><Family>.ts`. Implements the interface; exposes `info: { kind, ... }`; uses the lazy-load house pattern if it wraps a cloud SDK.
4. **Factory case** — one new `case` in `<family>/create<Family>.ts`'s switch. TypeScript fails the build if you forget.
5. **Env loader case** — one new branch in `<family>/config/load<Family>ConfigFromEnv.ts`'s kind-specific field reader.
6. **Optional dependency** — if the adapter wraps a cloud SDK, add the package to `cloudpdf/server/package.json`'s `optionalDependencies`.
7. **Test** — one file at `cloudpdf/server/test/<family>-<kind>.test.ts` covering construction, key operations, and that `info` exposes only public identifiers.

That's the entire surface. If a step doesn't apply (e.g., no cloud SDK → skip step 6), skip it explicitly in the PR description.

---

### Conventions Server Migrations (`docs/conventions/server-migrations.md`)

# Database migrations & rollback

`@cloudpdf/server` ships its schema as an ordered set of SQL migrations
(`NNN_name.sql`) for both SQLite and Postgres, applied by the migrator in
[`src/db/migrator/runner.ts`](../../cloudpdf/server/src/db/migrator/runner.ts). Each applied
migration is recorded in `schema_migrations` with a SHA-256 checksum of
its `up` SQL, so editing an already-applied migration is detected as
drift and refused at boot.

Every migration also ships an inverse `NNN_name.down.sql`. Forward
(`up`) is routine; backward (`down`) is a **manual break-glass tool**.

## Commands

```sh
cloudpdf-server migrate status                 # applied / pending / drift
cloudpdf-server migrate up                      # apply pending (forward)
cloudpdf-server migrate up --dry-run            # preview forward plan
cloudpdf-server migrate down --dry-run          # preview rollback plan
cloudpdf-server migrate down --steps 1 --yes    # roll back the newest migration
cloudpdf-server migrate down --to 011 --yes     # roll back everything newer than 011
cloudpdf-server migrate down --all --yes        # roll back the entire history
cloudpdf-server migrate validate [--strict]     # CI / boot drift gate
```

`migrate down` selection:

- `--steps N` — roll back the `N` highest applied migrations (default 1).
- `--to NNN` — roll back every applied migration **newer than** `NNN`,
  leaving `NNN` and everything below it applied.
- `--all` — roll back everything (down to an empty schema; the
  runner-owned `schema_migrations` table is preserved).

Safety:

- `migrate down` is destructive, so a real run **requires `--yes`**
  (containers are non-interactive). Use `--dry-run` to print the exact
  versions and order first.
- The whole revert set is validated **before** any DB change: each
  target must exist in code, its stored up-checksum must still match the
  code (otherwise pass `--force`), and it must declare non-empty `down`
  SQL. If any target is irreversible the command aborts without touching
  the database.
- `down` restores **structure, not data**. Re-creating a dropped column
  or table cannot bring back the rows/values that were in it. This is the
  standard rollback caveat and is harmless pre-launch.

## Rollback runbook (Docker / Helm)

Rollback is operator-initiated and runs as a one-shot, not via a Helm
hook:

```sh
# Preview first.
docker run --rm \
  -e CLOUDPDF_DB_DRIVER=postgres \
  -e CLOUDPDF_DB_URL=postgres://user:pass@host:5432/cloudpdf \
  ghcr.io/<org>/cloudpdf-server:<tag> \
  migrate down --to 011 --dry-run

# Then execute.
docker run --rm \
  -e CLOUDPDF_DB_DRIVER=postgres \
  -e CLOUDPDF_DB_URL=postgres://user:pass@host:5432/cloudpdf \
  ghcr.io/<org>/cloudpdf-server:<tag> \
  migrate down --to 011 --yes
```

**Ordering rule:** roll the application image back to a version that is
compatible with the _target_ schema **first**, then run `migrate down`.
Going down before the app is compatible can break live requests against
the older schema.

Prefer backward-compatible **expand/contract** migrations (add the new
column/table, deploy code that tolerates both shapes, only later drop the
old shape) so `down` stays a rare break-glass action rather than part of
the routine deploy.

## What never rolls back automatically

- Auto-migrate on boot (`CLOUDPDF_AUTO_MIGRATE`, default on for SQLite,
  off for Postgres) only ever applies **forward** migrations.
- `buildApp` validates for drift and can refuse to start
  (`CLOUDPDF_FAIL_ON_PENDING=1`) but never rolls anything back.
- There is no Helm pre-upgrade/rollback hook that runs `migrate down`.
  Down is exclusively the operator-run command above.

## Adding a migration

1. Add `NNN_name.sql` to **both**
   `cloudpdf/server/src/db/migrations/sqlite/` and
   `cloudpdf/server/src/db/migrations/postgres/`.
2. Add the matching `NNN_name.down.sql` to both dialects (reverse the up
   in child-before-parent FK order).
3. Register both in the dialect's `index.ts`
   (`{ version, name, sql, down }`).
4. Run `pnpm --filter @cloudpdf/server test` — the round-trip test
   (`cloudpdf/server/test/migrator-down.test.ts`) asserts
   `up → down --all → up` works on
   both dialects.

### SQLite `DROP COLUMN` caveat

SQLite refuses `ALTER TABLE ... DROP COLUMN` for a column referenced by a
`CHECK` constraint (or index/PK/FK/generated expression). When a down
needs to remove such a column, rebuild the table instead (see
`cloudpdf/server/src/db/migrations/sqlite/007_document_security.down.sql`):
mark the file
`-- pragma: no-transaction`, `PRAGMA foreign_keys=OFF`, create the
table's prior shape, copy, drop, rename, recreate indexes,
`PRAGMA foreign_keys=ON`. Postgres has no such limitation and drops the
columns directly.

---

### Conventions State And Sync (`docs/conventions/state-and-sync.md`)

# State and sync

Where a plugin keeps each piece of state, how that state changes, and how a
plugin's copy of engine data stays equal to what the engine confirmed. The
kinds of state are introduced in [`architecture.md`](./architecture.md); every
API below is a member of the `PluginContext` a plugin's `create(ctx)` receives
(`packages/core/main/src/types.ts`).

| Kind            | API                                                                         |
| --------------- | --------------------------------------------------------------------------- |
| Session         | `ctx.state` (`get`, `update`, `onChange`)                                   |
| Mirror          | `ctx.mirror(spec)`, `ctx.pageMirror(spec)`                                  |
| Pending changes | `ctx.changes` (`stage`, `hold`, `group`), a mirror's `predict` and `view()` |
| Resource        | closures, `ctx.notify()`, `ctx.cleanup`, `ctx.acquire`                      |

## Session state: `ctx.state`

```ts
interface StateCell<S> {
  get(): S;
  update<Args extends unknown[]>(
    transition: (state: S, ...args: Args) => S,
    ...args: Args
  ): void;
  readonly onChange: EventHook<StateChange<S>>; // { previous, next }
}
```

- The manifest's `state: () => S` builds the initial value, fresh for every
  instance. A plugin without session state omits it and its context is
  `PluginContext<void>`.
- The value is replaced, never mutated. It is plain data (see the rules in
  [`architecture.md`](./architecture.md#four-kinds-of-state)).
- It changes only through `update(transition, ...args)`. A transition is a
  named, exported, pure function in `model.ts`: `(state, ...args) => state`.
  Its name says what happens; there are no action objects or reducers.
- A transition that returns the same object is a no-op: no store
  notification and no `onChange`. Write transitions so they return `state`
  itself when nothing changes.
- `onChange` fires synchronously, before `update` returns, with the previous
  and next value. It is where a plugin derives its state-change events (see
  [`events.md`](./events.md)).
- After the instance closed, `update` drops the write and reports
  `instance-closed` once.

From `packages/plugin/shell`:

```ts
// model.ts
export function openMenu(state: ShellState, id: string): ShellState {
  if (state.openMenus.includes(id)) return state;
  return { ...state, openMenus: [...state.openMenus, id] };
}

export function closeMenu(state: ShellState, id: string): ShellState {
  if (!state.openMenus.includes(id)) return state;
  return { ...state, openMenus: state.openMenus.filter((menu) => menu !== id) };
}

// controller.ts
openMenu: (id) => ctx.state.update(openMenu, id),
closeMenu: (id) => ctx.state.update(closeMenu, id),
```

## `ctx.notify()`

Wakes every subscriber of the store's change stream without changing state.
Call it when a resource changed and reads through the capability now answer
differently: a registry gained an entry, a raster arrived, a policy changed.
It is a no-op once the instance closed.

```ts
// packages/plugin/commands/src/controller.ts: definitions hold functions,
// so the registry is a closure map; every change wakes the readers.
ctx.notify();
```

## `ctx.watch(select, handler, isEqual?)`

Runs `handler(value, previous)` whenever `select()` answers differently after a
store change (`Object.is` unless `isEqual` is given). It never runs for the
initial value. Use it to react to state this plugin does not own: the page
registry, a sibling's getter. A plugin observes its own state with
`ctx.state.onChange` instead. The subscription ends when the instance closes;
the returned `Unsubscribe` ends it earlier.

```ts
// packages/plugin/stage/src/connect.ts
ctx.watch(
  () => ctx.document()?.revision ?? 0,
  () => stage.refit(),
);
```

## `ctx.waitFor(predicate, options?)`

Resolves once `predicate()` holds, checked immediately and after every store
change. Rejects with `operation-cancelled` when `options.signal` aborts, with
`instance-closed` when the instance closes, and with the predicate's own error
if it throws. A verb never uses it to see its own engine write (see
[the rules](#rules)).

## Mirrors: `ctx.mirror(spec)`

A mirror is a local copy of document-wide data the engine owns. It changes in
two ways only: a load (the whole value, or some pages of it) and a fold (one
confirmed event applied by a pure function). Mirrors exist in
document-scoped plugins only.

| Spec member                      | Meaning                                                                                                                                                                               |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                           | Unique within the plugin; names the store cell and appears in errors.                                                                                                                 |
| `initial()`                      | The value before the first load lands.                                                                                                                                                |
| `readable?()`                    | Asked before every load. `false`: status `forbidden`, no engine read.                                                                                                                 |
| `load(doc, signal)`              | Read the whole value: `{ value, cursor? }`. `cursor` is the newest server event the snapshot already contains (cloud engines); omit it otherwise.                                     |
| `fold(value, event)`             | Apply one confirmed event. Pure, the same for every origin. Returns `value` itself when the event does not apply, or `reload(...)` when the event does not carry enough to apply.     |
| `predict?(value, op)`            | Apply one op of this session's pending changes, as the engine will. Pure and absolute (see [Pending changes](#pending-changes)). Returns `value` itself for an op it doesn't concern. |
| `loadPages?(doc, pages, signal)` | Read some pages and return `(value) => value` merging them in. Needed when `fold` returns `reload({ pages })`.                                                                        |
| `changed?(change)`               | Runs after every load (even one that changed nothing) and after every event that changed the value. The one place the plugin emits the events this data drives.                       |

The metadata plugin is a complete mirror plugin in one file
(`packages/plugin/metadata/src/controller.ts`):

```ts
const metadata = ctx.mirror<DocumentMetadata | null>({
  name: 'metadata',
  initial: () => null,
  load: async (doc) => ({ value: await doc.metadata.get() }),
  fold: (value, event) =>
    event.type === 'metadata.updated' ? event.metadata : value,
  changed: ({ cause, event, previous, next }) => {
    if (!next) return;
    if (cause === 'load') {
      resynced.emit({ metadata: next });
    } else if (event && 'origin' in event) {
      updated.emit({
        metadata: next,
        previous,
        changedKeys: changedKeys(previous, next),
        origin: event.origin,
      });
    }
  },
});
```

A fold with several cases lives in `model.ts` as a named function, so it can
be tested without a kernel (`packages/plugin/form/src/model.ts`):

```ts
export function foldFormEvent(
  index: FieldIndex,
  event: DocumentEvent,
): FieldIndex | MirrorReload {
  switch (event.type) {
    case 'forms.valueSet':
    case 'forms.updated':
    case 'forms.widgetAdded':
    case 'forms.widgetRemoved':
      return upsertFields(index, [event.field]);
    case 'forms.deleted':
      return removeField(index, event.deletedFieldObjectNumber);
    case 'forms.imported':
      return indexFields(event.snapshot);
    case 'forms.repaired':
      return reload();
    // …
    default:
      return index;
  }
}
```

### The mirror's life

1. **Subscribe first.** The mirror subscribes to `ctx.doc.events` when it is
   created, inside `create`.
2. **Load after connect.** The first load starts right after the plugin's
   `connect` has run, so capabilities that `readable` or `load` use exist.
   A spec with `onDemand: true` waits instead: the mirror stays `idle`,
   folding confirmed events, until `ensureLoaded()` or `refresh()` asks for
   its first load, for data nothing needs when a document opens. A resync or a
   fold's `reload()` before then reads nothing; the first load reads it all.
3. **Queue during a load.** Events that arrive while a full load runs are
   queued. When the load lands, the queued events are replayed in arrival
   order, skipping those the snapshot already contains
   (`origin.serverId <= cursor`). Replaying confirmed records in order is
   idempotent, so engines without a cursor replay everything.
4. **Degrade on failure.** A failed load keeps the previous value, applies the
   queued events to it, and reports `forbidden` (a `permission-denied`
   rejection) or `error` through `getStatus()`. The mirror stays live. A
   failed page reload does the same: the value is stale for those pages, and
   says so, until a full load succeeds.
5. **Resync.** `stream.desynced` and `document.versioned` start a full reload.
   `fold` never sees them.
6. **Throwing folds.** A `fold` that throws is reported and starts a full
   reload.
7. **Coalescing.** A reload requested while one runs schedules exactly one
   more after it.
8. **Storage.** The value lives in a store cell of its own
   (`<plugin id>::<document id>/mirror/<name>`), revoked when the instance
   closes. It is reactive through the ordinary change stream, and a closed
   instance cannot write it.

The kernel tests each step in `packages/core/main/test/mirror.test.ts`.

### Partial reloads

`fold` returns `reload()` to read everything again, or `reload({ pages })` to
read only some pages through `loadPages`. Without `loadPages`, a page-scoped
request reloads everything. The annotation records mirror
(`packages/plugin/annotation/src/sync/records.ts`) re-reads only the pages a
redaction or a flatten touched:

```ts
case 'redaction.applied':
case 'pages.flattened': {
  const pages = event.results
    .filter((result) => result.status === 'applied')
    .map((result) => result.page);
  return pages.length ? reload({ pages }) : records;
}
```

### What `changed` receives

```ts
interface MirrorChange<V> {
  readonly cause: 'load' | 'event';
  readonly event: DocumentEvent | null; // null for loads
  readonly pages: 'all' | readonly PageRef[] | null; // loads only: what was read
  readonly previous: V;
  readonly next: V;
}
```

### Status and control

| Member           | Behavior                                                                                                                                                                 |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `get()`          | The value the engine confirmed.                                                                                                                                          |
| `view()`         | What the user sees: `get()` with this session's pending changes replayed on top through `predict`. `get()` when the spec has no `predict`.                               |
| `getStatus()`    | `idle` until the first load starts, `loading` while it runs, then `ready`. Stays `ready` while a reload runs. `forbidden` or `error` after a failed load or page reload. |
| `ensureLoaded()` | Load unless loaded or loading: shares a load in flight, resolves at once when loaded, tries again after a failed load. Rejects when the load fails.                      |
| `refresh()`      | Reload everything. Joins a running load; rejects when the load fails.                                                                                                    |
| `settled()`      | Resolves once no load or page reload runs, including reloads started while it waits. Never rejects.                                                                      |

A capability usually passes these through: `getStatus: metadata.getStatus`,
`refresh: () => metadata.refresh()`.

## Page mirrors: `ctx.pageMirror(spec)`

A page mirror holds engine data that is read page by page, on demand. A page
is `idle` until something calls `ensureLoaded(page)`; once loaded, it stays
current.

| Spec member                 | Meaning                                                                                                                                                                      |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                      | Unique within the plugin.                                                                                                                                                    |
| `load(doc, page, signal)`   | Read one page's value.                                                                                                                                                       |
| `affected(event)`           | Which pages an event makes stale: a list, `'all'` (every loaded page), or `null`. Only loaded pages are touched.                                                             |
| `fold?(value, event, page)` | Apply an event to an affected page instead of re-reading it; return `'reload'` to re-read. Without `fold`, affected pages are re-read.                                       |
| `predict?(value, op, page)` | Apply one op of this session's pending changes to a loaded page's value, as a mirror's `predict` does.                                                                       |
| `changed?(change)`          | Runs after every load of a page (even one that changed nothing) and every applied change. `change` is `{ page, cause: 'load' \| 'event' \| 'drop', event, previous, next }`. |

| Member               | Behavior                                            |
| -------------------- | --------------------------------------------------- |
| `get(page)`          | The page's value, or `undefined` before it loaded.  |
| `view(page)`         | `get(page)` with the pending changes on top.        |
| `getStatus(page)`    | `idle`, `loading`, `ready`, `forbidden` or `error`. |
| `ensureLoaded(page)` | Load the page unless it is loaded or loading.       |
| `refresh(page?)`     | Re-read one page, or every loaded page.             |

Built in:

- a load for a page that is already loading shares the running read;
- every read and fold bumps a per-page epoch, so an older read never
  overwrites a newer value;
- `pages.deleted` drops the deleted pages (`cause: 'drop'`);
- `stream.desynced` and `document.versioned` re-read every loaded page;
- a failed read keeps the page's previous value and sets its status.

From `packages/plugin/link/src/controller.ts`:

```ts
const pages = ctx.pageMirror<readonly Link[]>({
  name: 'links',
  load: async (doc, page) => {
    const layout = ctx.getPage(page);
    if (!layout) {
      throw new PluginError(
        'not-found',
        'link',
        `page ${page.objectNumber} is not in this document`,
      );
    }
    const snapshot = await doc.page(page).annotations.list();
    return linksOf(snapshot.annotations, page, layout.boxes.crop);
  },
  affected: pagesChangedBy,
  changed: ({ page, cause }) => {
    if (cause === 'load') loaded.emit({ page });
  },
});
```

## Pending changes

A user action is one change: a list of the engine's ops (`ChangeOp`),
applied all or nothing by `doc.apply`, sent as one request, undone as one
step. A plugin describes its writes as ops and stages them on the document's
change queue, `ctx.changes`, which every plugin of the document shares:

```ts
// Moving three selected annotations.
ctx.changes.stage({
  label: { key: 'annotation.move', count: 3 },
  ops: moved.map(({ ref, rect }) => ({
    type: 'annotations.update',
    ref,
    patch: { rect },
  })),
  undo: before.map(({ ref, rect }) => ({
    type: 'annotations.update',
    ref,
    patch: { rect },
  })),
});
```

- **Shown at once.** Before `stage` returns, every mirror's `view()` shows the
  change: the mirror replays the pending changes over its value, op by op,
  through its spec's `predict`.
- **Sent in order.** The queue sends a change after everything staged before
  it; the engine keeps that order. `stage` returns the `PendingChange`: its
  `opId` (what its events carry as `origin.tx.id`) and `result`, the engine's
  answer.
- **`label`** names it in history and refusals: an i18n key and its values.
  **`undo`** says what undoing it looks like, so a history can draw it; it is
  never sent. `history: false` keeps a change out of the history.

### Predict

`predict` is the engine's own rule for an op, from the engine-core functions
the engine follows, next to the mirror's `fold` (the annotation plugin's
`predictRecords`, in `sync/records.ts`).

- **Pure and absolute.** Ops say what to set, never by how much, so replaying
  an op over a value that already holds its answer changes nothing. Both
  engines publish a change's events before they answer, so for a moment the
  mirror holds the answer and the change is still pending; with absolute ops
  that moment looks exactly like the answer.
- **Keep what an op doesn't touch.** Return the same object for every record
  an op leaves alone, so readers of the other records don't run again.
  `view()` is memoized on the value and the pending list.
- **Reads show the view.** What a plugin's reads hand out (`list`, `get`, its
  render items) comes from `view()`. `get()` stays the confirmed truth, for
  code that must not see a prediction.

### When a change leaves the view

| The engine…         | The change leaves…                                                                                                                                                                                                                          |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| applied it          | each mirror's view once that mirror holds the answer: once it folded the change's last event, even before the answer arrives; after the read a `fold` asked for, when it asked; and a mirror that couldn't read keeps it until a load lands |
| refused it          | every view, at once, together with its dependents                                                                                                                                                                                           |
| the document closed | nothing is shown any more; its `result` rejects `instance-closed`                                                                                                                                                                           |

**Dependents.** A create can name its object (`objectNumber`, from
`ctx.changes.takeObjectNumber()`), so later changes can refer to it before the
engine answers. When that create is refused, the changes that refer to what
only it would have created can't apply: the queue refuses them too (`conflict`,
`details.reason: 'dependency-refused'`) and aborts their engine calls.
`onSettled` reports every answer, refusals with their label.

### Holds and groups

- **A hold** is a change that can still be amended before it is sent: typing,
  or a form commit waiting for its scripts. `ctx.changes.hold(label)` returns it;
  `set(ops, undo?)` replaces its ops, and the views show the latest. Any later
  `stage` sends the open holds first, and `send()` sends the holds staged
  before it too, so send order is always staging order. `cancel()` drops it.
  `open` says whether it can still be amended: once sent, what comes after
  goes into a new hold. `change` is the change it is once it has ops (its
  `opId`, its answer). The plugin decides when it's done (a pause in typing,
  `ctx.clock.after`); the kernel sends every open hold before a download.
- **A group** makes everything staged while its `run` runs, by any plugin, one
  change: `ctx.changes.group(label, () => { … })`. Only what is staged before
  `run` returns joins; a verb that awaits first stages its own change. Nested
  groups join the outer one, a hold inside a group is refused, and when `run`
  throws nothing of it is sent.

### One change, one store update

Both engines publish a change's events in one burst that shares `origin.tx`.
The kernel holds the store's notifications while a burst arrives, so readers
see a change land in one update, whoever made it.

The kernel tests each rule in `packages/core/main/test/changes.test.ts`.

The annotation plugin is the reference: one change per message
(`services/store.ts`), its records mirror predicts (`sync/records.ts`), new
records are named by the object numbers they take, and typing is a hold
(`write/text-editing.ts`).

### Undo and redo

The history (`@embedpdf/plugin-history`) is built on the queue and keeps no
copy of the document. Every change staged on the queue is a step, recorded
when it is staged (`onStaged`), so an undo right after an action undoes it
while it is still on its way. An undo is a change too:
`stage({ label, undoOf: opId, shows })`. The views show `shows` (the change's
`undo`) at once, and the engine applies the reverse it recorded. A redo is the
undo of that undo.

- **What a plugin gives the history:** `undo` on every change it stages, so an
  undo is drawn before the engine answers, and `history: false` on a change
  that isn't the user's action. Holds that make up one step pass the same
  `merge` key: `hold(label, { merge: 'annotation.text:<id>' })`.
- **What a plugin may do around it:** listen to `onStaged` for changes whose
  `undoOf` is set, as the annotation plugin does to put the selection back
  (`services/selection-history.ts`). It never stages undos itself.
- **What the engine decides:** a part someone changed since is left alone
  (`skipped`), a final change (redaction, flattening, signing, form repair)
  or a new version ends undo before it (`conflict` with the engine's
  `UndoUnavailable`), and an undo of a refused change applies nothing.

## Held-back writes

Some writes wait on purpose: the annotation plugin sends typed text once
typing pauses (a hold), the ink tool waits for the next stroke of a drawing, a
form field is written on blur. A hold (see [Pending changes](#pending-changes)) is
how a change waits, and the kernel sends every open hold before a download
itself. A write queue holds writes that are on their way.
Before anything reads the whole file (`documents.download()`, `downloadLayer()`), or draws
its pages to print them (`documents.preparePrint()`, `print()`), the kernel settles the document (`packages/core/main/src/settle.ts`): it runs every
flush plugins registered with `ctx.onSettle(flush)` and waits for them, so the
file has everything the user sees.

- A flush sends what the plugin holds back and resolves once the engine has
  it: the ink draft finished between strokes (never a stroke still being
  drawn), the field being typed in committed (matches Acrobat: the field being
  edited is committed before a save). Holds need no flush: the kernel sends
  them.
- A plugin whose queue carries document writes registers it:
  `ctx.onSettle(() => queue.idle())`. A queue whose operations read the file
  themselves is never registered: the actions plugin runs every download
  inside its queue (`ctx.aroundDownload`, for the document's save actions,
  and `ctx.aroundPrint`, for its print actions), and waiting for that queue
  would wait for itself.
- A failed flush is reported and does not stop the read: the file is then what
  the engine has.
- The caller's `signal`, or the document closing, cancels the wait with
  `operation-cancelled`.

## Resources

Anything that is not plain data is a resource: engine handles, rasters,
workers, script realms, registries of functions, caches. A resource lives in a
closure inside the controller.

- Acquire it with `ctx.acquire(get, dispose)` or pair it with
  `ctx.cleanup(dispose)` on the line after acquiring it (see
  [`plugins.md`](./plugins.md#lifetime-and-async)).
- When it changes in a way reads can see, call `ctx.notify()`.
- Never put it into `ctx.state` or a mirror.

## Reactions

A reaction responds to confirmed document events without keeping a copy of
the data. It subscribes in `connect`, ignores origin for what it does, and
touches only its own caches and operations. The render plugin maps each event
to the pixels it changes (`packages/plugin/render/src/invalidation.ts`) and
invalidates them:

```ts
connect() {
  ctx.listen(ctx.doc.events, (event) => {
    const change = pixelChangeOf(event, allPageObjectNumbers);
    if (!change) return;
    publishInvalidation(change.pages, change.scope, 'origin' in event ? event.origin : null);
  });
},
```

Other reactions: search re-runs its query after any confirmed mutation, the
signature plugin re-judges its verdicts after edits of the working copy, and
the actions plugin clears its per-page trigger cache.

## Rules

1. A mirror changes only by `fold` and by loads. Verbs never write it.
2. `fold` is pure and ignores origin. Origin may shape presentation, never
   data, and only outside the fold: the annotation plugin renders another
   session's change from the engine's baked appearance, and records this
   session edited or created from their description (a session preference,
   `vector` in its state).
3. `fold` applies the data the event carries. It asks for a reload only when
   the event does not carry enough (`forms.repaired`, or `redaction.applied`
   for annotations).
4. Verbs stage a change (or call the engine) and return. They do not
   refetch, do not wait for revisions, and do not emit fact events.
5. A verb's `await` sees its own write. Both engines publish the event for a
   session's own mutation before the mutation's promise settles, so the fold
   and the fact events have run by the time the verb resumes.
6. Fact events come from `changed` (or from the plugin's one listener for that
   event) with `event.origin`. Loads announce `onResynced`.
7. Reactions subscribe with `ctx.listen` in `connect`.
8. `refresh()` exists for recovery and for callers that want a fresh read. No
   write path calls it.
9. What the user sees is `view()`: the truth with this session's pending
   changes on top. Nothing else keeps a copy of a pending change.

---

### Conventions Testing (`docs/conventions/testing.md`)

# Testing

Where tests live, which layer to test at, and the tools the kernel and the
engine provide for it.

## Where tests live

- A package's tests live in `test/`, next to `src/`, in the same structure:
  `src/sync/records.ts` is tested in `test/sync/records.test.ts`. Tests that
  run several areas together are named `*.integration.test.ts`; shared
  fixtures and helpers go in `test/fixtures/` and `test/helpers/`.
- Test names are sentences that describe behavior.
- Tests run with Vitest: `pnpm --filter <package> test`, or `pnpm test` for
  the whole repository.
- Vitest does not typecheck. `pnpm typecheck` checks a package's tests when
  its `tsconfig.json` lists `test` in `include`, as most plugins do.

## Pick the layer

| What                                        | How                                                           |
| ------------------------------------------- | ------------------------------------------------------------- |
| Transitions, projections, folds             | Call the pure functions in `model.ts` (or `sync/`) directly.  |
| A controller: reads, verbs, events, mirrors | `createTestContext` from `@embedpdf/core/testing`.            |
| Bring-up, dependencies, document lifecycle  | `createKernel` with a fake engine.                            |
| Engine behavior plugins rely on             | A conformance suite from `@embedpdf/engine-core/conformance`. |

## The test context

`createTestContext(options)` returns a real `PluginContext`: the kernel's own
store, state cell, mirrors, page mirrors and event sources, page geometry
built from a page list, capabilities from a token map, and a document handle
shaped by the test.

| Option         | Meaning                                                                                                                                                                          |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`           | The plugin id (`ctx.id`, error messages). Default `'test'`.                                                                                                                      |
| `state`        | The initial session state; omit for a stateless plugin.                                                                                                                          |
| `settings`     | The plugin's settings as its definition declares them, `{ defaults, registered? }`; `ctx.settings()` reads them. Without them, `ctx.settings()` doesn't compile.                 |
| `pages`        | The document's pages: `{ ref, size?, crop?, rotation?, userUnit?, label? }`. `document()`, `getPage` and `geometry` derive from them.                                            |
| `capabilities` | `[token, capability]` pairs that `get` and `tryGet` resolve.                                                                                                                     |
| `doc`          | The document handle members the test uses (`metadata`, `forms`, `page`, …). The default has an event stream and `security.allows` answering true. `null` for a workspace plugin. |
| `documentId`   | Default `'doc'`.                                                                                                                                                                 |
| `engine`       | The `ctx.engine` members the test uses.                                                                                                                                          |
| `clock`        | The host's time behind `ctx.clock`. Default `timerClock`: the environment's timers (so `vi.useFakeTimers()` applies) and no frames. Pass `manualClock().clock` to step time.     |

The returned context adds:

| Member                     | Behavior                                                                                                           |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `connect(instance)`        | Does what the kernel does after `create`: runs `connect`, then starts the mirrors' first loads. Returns the `api`. |
| `emitDocumentEvent(event)` | Delivers a confirmed document event through the default `doc.events`.                                              |
| `subscribe(listener)`      | Observes the change stream (state updates and `notify`).                                                           |
| `capabilities`             | The token map, to add or replace capabilities during a test.                                                       |
| `settle(signal?)`          | Runs every `ctx.onSettle` flush and waits for them, as `documents.download()` does before it reads the file.       |
| `download(read)`           | Runs `read` inside every `ctx.aroundDownload` wrap, as `documents.download()` does after settling.                 |
| `print(print)`             | Runs `print` inside every `ctx.aroundPrint` wrap, as a prepared print does around the print dialog.                |
| `dispose()`                | Aborts the lifetime and runs every registered cleanup.                                                             |

`manualClock(options?)` from `@embedpdf/core/testing` is time that moves only
when the test moves it: `advance(ms)` runs the timers that fall due, earliest
first; `frame(timeMs?)` moves to that time and runs the callbacks waiting for a
frame; `pending` counts what waits. `manualClock({ frames: false })` is a host
that never paints. The same clock goes to `createKernel({ clock })` for a test
through the kernel.

Unless the test passes its own, `DocumentsToken` resolves to a read-only
registry holding the one document; its mutating verbs throw `unsupported`. The
test context does not check declared dependencies; a test through
`createKernel` does.

The smallest harness (`packages/plugin/shell/test/shell.test.ts`):

```ts
const harness = () => {
  const ctx = createTestContext({ id: 'shell', state: initialShellState() });
  return ctx.connect(createShellController(ctx));
};
```

A harness for a plugin with a mirror, here the title plugin from
[`plugins.md`](./plugins.md#a-minimal-complete-plugin):

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Fake engines publish before they resolve

Both engines publish the event for a session's own mutation before the
mutation's promise settles, and plugins depend on it: a verb's `await` sees
its own write. A fake document handle does the same. A fake that resolves
first, or never publishes, tests a system that does not exist.

```ts
// packages/plugin/metadata/test/metadata.test.ts
update: vi.fn(async (patch: { title?: string | null }) => {
  const metadata = META({ title: patch.title ?? null });
  const result = { metadata, cache: null };
  // Like both real engines: the confirmed event is published before the promise settles.
  emit({ type: 'metadata.updated', origin, ...result });
  return result;
}),
```

## Mirror tests

- **Fold tests.** A fold is a pure function; test each event case directly,
  including the ones that return the value unchanged and the ones that ask for
  a reload (`expect(fold(value, event)).toEqual(reload({ pages }))`).
  `packages/plugin/annotation/test/sync/records.test.ts` is the reference.
- **Parity tests.** The same event with `origin.kind: 'local'` and with
  `'remote'` produces the same mirror value. Origin may change presentation,
  never data.
- **No refetch.** Count the engine's reads: a mirror reads once when it loads,
  not again after a write (`packages/plugin/form/test/fields-mirror.test.ts`).

## Pending change tests

Test what the user sees while a change is on its way and after the engine
answered, through the capability, with a fake engine that answers changes in
the order they were sent (as both engines do), can refuse one, hold it in
flight, and deliver another session's event meanwhile. A refused change must
show the mirror's record, including that other session's change; an answer
must never replace a newer change still on its way.
`packages/plugin/annotation/test/pending.test.ts` is the reference, and
`test/interleavings.test.ts` plays seeded random sequences of the same.

- **Events.** A write fires its fact event once, with the right origin; a load
  fires `onResynced` and no per-item events.

The kernel tests the mirror protocol itself (loading after connect, queueing
and cursor replay, failures, resyncs, coalescing, closing) in
`packages/core/main/test/mirror.test.ts` and `page-mirror.test.ts`; a plugin
does not repeat those tests.

## Kernel tests

A test that needs bring-up, dependency resolution or the document lifecycle
creates a real kernel with a fake engine and opens a document:

```ts
const kernel = createKernel({
  engine,
  plugins: [interactionPlugin(), formPlugin()],
});
await kernel.start();
await kernel.documents.open({
  kind: 'bytes',
  id: 'd',
  bytes: new Uint8Array(),
});
const form = kernel.capability(FormToken, 'd');
```

Destroy the kernel at the end of the test (`await kernel.destroy()`).

## Engine conformance suites

`@embedpdf/engine-core/conformance` exports one suite per engine feature
(`runDocumentEventsConformance`, `runFormConformance`,
`runAnnotationMutationConformance`, …). Each takes a test runner and options,
and every engine runs every suite: the local engine in
`packages/engine/main/test/`, the cloud engine in `cloudpdf/engine/test/`, both
in the engine conformance workflow in CI. Behavior a plugin relies on is
pinned by a conformance test, so both engines must provide it.
`runDocumentEventsConformance` checks that every confirmed mutation publishes
exactly one event with the caller's result, that failed mutations publish
nothing, that own mutations are `local` with a stable session id, that
unsubscribing stops delivery, and that own mutations are published before
their promise settles.

## Type tests

A test file may assert that a shape does not compile with
`// @ts-expect-error`. If the shape starts compiling, `pnpm typecheck` fails
with an unused-directive error, so the assertion cannot rot
(`packages/plugin/annotation/test/tools/definitions.test.ts`). This works in
packages whose `tsconfig.json` includes `test`.

---

### Conventions Viewer Doors (`docs/conventions/viewer-doors.md`)

# Doors — the law for the viewer product line

[`packages.md`](./packages.md) is the law for where a package lives, and
[`tooling/build/README.md`](../../tooling/build/README.md) for how packages build. This file is the law for
**how one viewer serves several deliveries**: EmbedPDF with the built-in
engine, CloudPDF with a remote one, a CDN snippet, and one wrapper per
framework — without forking the viewer once.

A **door** is an entry point that decides one thing: which engine is in the
bundle. Everything else is shared, names included: the CloudPDF viewer is the
same `<embedpdf-viewer>`, `init()`, `<PDFViewer>` and `<epdf-pdf-viewer>` with
the cloud engine as the default, as `@embedpdf/viewer` is with the built-in one.

Unqualified `kernel/`, `local/`, and `doors/` paths below are relative to
`packages/viewer/main/src/`.

## The five laws

1. **One artifact, many doors.** There is a single viewer implementation. A
   door is an entry file, not a copy — if you are about to duplicate component
   or element code per delivery, stop; the difference belongs in a door.

2. **The kernel is door-blind.** Nothing under `kernel/` may import from
   `doors/` or `local/`, and the word "local" may not appear there. Same rule
   one level up: a framework wrapper's `component.*` imports the viewer for
   types only (`import type`), so it holds no runtime edge to any engine.

3. **A door declares its own truth.** A config type may only promise what that
   door's imports actually deliver. The local door's `engine` is optional and
   accepts the built-in engine's options bag; the `/core` door's is required and
   takes only a real `Engine | EngineFactory`. Both are called `ViewerConfig` —
   one name per door, so samples and docs never pick a door-specific type name.
   An engine's OPTIONS type lives next to the code that interprets it
   (`LocalEngineConfig` in `viewer/main/src/local`, `CloudEngineOptions` in
   `@cloudpdf/engine`), never in shared code.

4. **Doors never import doors.** Two deliveries that share a surface share a
   module, not each other — that is why `local/surface.ts` exists and the npm,
   portable and snippet doors all re-export it, differing only in the engine
   entry and the wasm default they register.

5. **The implementation sits at the widest type; doors narrow by assignment.**
   Parameter types are contravariant, so a function taking the wider config
   satisfies a narrower signature — never the reverse. Hence `initViewer` and
   `<PDFViewer>` are typed with the kernel's `ElementConfig` (engine seam
   `unknown`), and each door re-types them by assignment:

   ```ts
   const init: (options: InitOptions) => EmbedPdfViewerElement = initViewer;
   ```

   **If you find yourself writing `as` here, you have it backwards.**

## Why the element's engine seam is `unknown`

`EmbedPdfViewerElement` is one compiled class serving every door, so its
`.config` must carry a value only the sending door understands. It opens the
envelope far enough to recognise an `Engine` or a factory, and otherwise hands
it to the provider its door registered (`EmbedPdfViewerElement.defaultEngineProvider`,
set by `registerLocalEngine()` or `@cloudpdf/viewer`'s `registerCloudEngine()`;
the `/core` door sets none, which is exactly what makes its `engine` required).

What the provider returns (`DoorEngine`) is the engine, and what the element
can't know about it: the fonts its settings name, how a `src` in that engine's
words opens (`sourceOf`: the cloud's `{ token }` is the engine's
`{ kind: 'token' }`; the element reads only URLs, files, bytes and open inputs
itself), and plugin settings the engine needs under the config's own
(`plugins`: the cloud's stamp libraries open in a local engine). The element
has one default engine, so an app imports one door.

Typing that seam narrower would either forbid a legal door vocabulary or
advertise one a different door cannot honour. So the imperative `el.config =`
path stays opaque and runtime-checked on purpose; typed config belongs at
`init()` and `<PDFViewer>`, which are per-door.

## The shape

```
packages/viewer/main/src/
  kernel/     door-blind: element, config base, init, public surface
  local/      the built-in engine's vocabulary + registration (not an entry)
  doors/      entries only — local.ts, portable.ts, core.ts, snippet.ts
```

Every framework wrapper is the same four files, in this order:

```
src/component.ts   the wrapper, engine-blind, `import type` only
src/parts.ts       `./component`: the component at the widest config, and the
                   components and hooks every door shares
src/index.ts       the local door:  import '@embedpdf/viewer';       + parts + narrowed props
src/core.ts        the core  door:  import '@embedpdf/viewer/core';  + parts + narrowed props
```

(Angular's are entry folders: `component/`, `core/`, and `src/` for the local door.)
Read one wrapper and you have read them all.

## Doors in another package

`@cloudpdf/viewer` and its wrappers are doors too, the same shape across a
package line: `@cloudpdf/viewer` registers the cloud engine and is otherwise
`@embedpdf/viewer/core`'s surface with the cloud's `ViewerConfig`; each
`@cloudpdf/viewer-<framework>` imports it, re-exports
`@embedpdf/viewer-<framework>/component`, and narrows the component to the
cloud's config. Its npm build leaves every `@embedpdf/*` package to the app's
bundler, so the app has one element class, the one the engine is registered on.

## Checklist: adding a framework wrapper

1. `packages/viewer/<framework>` — the name falls out of [`packages.md`](./packages.md)
   (`@embedpdf/viewer-<framework>`).
2. `component.*`: types only from `@embedpdf/viewer/core`; props are
   `ElementConfig & <Framework>Extras`. Set `.config` before the element's
   deferred mount, so the viewer boots exactly once — make that an acceptance
   test.
3. `parts.*` (`./component`): the component and every framework part, for
   doors in this package and others. `index.*` / `core.*`: one side-effect
   import each, re-export the parts, then the component narrowed to that
   door's `ViewerConfig`.
4. `package.json`: add `"./core": "./src/core.*"` to `exports` (the build
   derives entries from it — see `tooling/build/README.md`) and list both door
   entries in `sideEffects`. A pure re-export barrel with `sideEffects: false`
   can be dropped by a bundler, taking the element registration with it.
5. Children pass through as light DOM. There is no slot bridge: a slot is a
   child.
6. Prove both doors. A probe of `@ts-expect-error` lines is self-validating —
   if a door stops rejecting a bad shape, tsc fails with "unused directive".
   Assert the bundle too: the `/core` door must emit no `FPDF_` symbols and no
   wasm asset.

## Enforcement

`eslint.config.js` carries the import-boundary rules for laws 2 and 3
(`component.*` is `import type` only; `cloudpdf/**` may not import the local doors).
They are **latent until lint is wired up in CI** — there is no root
`tsconfig.json` and no `lint` script today, so treat the tree and the type
system as the real guardrails until that lands.

---

### Content Engine Authentication (`docs/content/engine/authentication.mdx`)

---
title: Authentication
description: Open a document with a token from your backend, by id, or with a public share link, and what a token carries.
engines: [cloud]
---

import { Callout } from '@/components/docs/callout';

# Authentication

Every request the engine makes carries a token. Your backend creates the token and signs it with
your secret key; the engine only passes it along, and the server checks it. The token says which
document the user may open, what they may do with it, and who they are.

## Three ways to open a document

Pick the one that matches where the credential comes from.

### With a document token

Your backend creates a token for one user and one document. This is the most common way:

```ts
const doc = await engine.open({
  kind: 'token',
  token: () => fetchDocumentToken(documentId),
});
```

The token names the document, so you don't pass an id. Pass a function rather than a string: the
engine calls it for every request, so a short-lived token never runs out mid-session.

One engine can hold many documents opened this way, each with its own token.

### By id

When your app already has a token for your whole account (an admin tool, a backend job), open any
of its documents by id:

```ts
const engine = cloudEngine({ token: () => fetchAccountToken() });

const doc = await engine.open({ kind: 'id', id: 'doc_proposal_2026' });
```

The engine's `token` is used for every document opened by id. To use a different one for a
single document, pass `token` in the input as well.

### With a share link

A share link (`shr_…`) comes from the CloudPDF dashboard. It needs no backend at all, which makes
it right for public embeds:

```ts
const doc = await engine.open({ kind: 'share', shareToken: 'shr_9f2k…' });
```

A share link isn't a credential itself: the engine exchanges it for a short-lived token, and
renews that token before it expires. Editing or revoking the share in the dashboard changes every
embed at the next renewal. The server only accepts the exchange from the websites the share
allows.

If the share has a passphrase, pass it as `sharePassword`. Without it, `open()` fails with
`SharePasswordRequired`, so you can ask the user and try again:

```ts
const doc = await engine.open({ kind: 'share', shareToken, sharePassword: 'open-sesame' });
```

## Password-protected PDFs

A PDF can have its own password, separate from any token or share passphrase. Pass it in the
options, whichever way you open:

```ts
const doc = await engine.open({ kind: 'token', token }, { password: 'pdf-password' });
```

[Opening documents](/docs/engine/documents/opening#password-protected-files) shows how to ask
for it only when a file needs one.

## What a token carries

A token carries the user's **permissions** (for example `doc.render`, `doc.text.copy`,
`annotations:update:self`) and their **identity** (a user id, a display name, a group). The
server enforces the permissions on every request; the identity becomes the author of the
annotations the user creates.

Both come from the token, so the engine has no options for them. Read what the user was given
from `doc.security`:

```ts
doc.security.allows('doc.download'); // may this user download the file?
doc.security.identity?.displayName; // who they are
```

[Permissions](/docs/engine/concepts/permissions) lists every permission and how to build UI that
matches them.

<Callout type="warn">
  Create tokens on your server, never in the browser, and keep them short-lived. A document token
  opens one document only, so a leaked token can't be used for any other.
</Callout>

## When something is wrong

A missing or expired token fails with `Unauthenticated`; a token without the permission a call
needs fails with `Forbidden`. See [Errors & cancelling](/docs/engine/concepts/errors-and-cancelling).

---

### Content Engine Quick Start (`docs/content/engine/quick-start.mdx`)

---
title: Quick start
description: Open a PDF, render its first page, read and search its text, highlight a match and save the result.
---

import { Cards, Card } from '@/components/docs/cards';
import { Callout } from '@/components/docs/callout';

# Quick start

This guide opens a PDF, shows its first page, searches it, highlights a match and saves the
result.

## 1. Install

<Engine local>

```sh npm2yarn
npm install @embedpdf/engine
```

</Engine>

<Engine cloud>

```sh npm2yarn
npm install @cloudpdf/engine
```

</Engine>

## 2. Create the engine

Create one engine for your whole app and reuse it.

<Engine local>

```ts
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
```

Creating it is instant. The engine starts PDFium in a Web Worker the first time you use it, so
you don't have to wait for anything here.

</Engine>

<Engine cloud>

```ts
import { cloudEngine } from '@cloudpdf/engine';

const engine = cloudEngine();
```

It connects to CloudPDF SaaS. Your own server names itself:
`cloudEngine({ baseUrl: 'https://pdf.example.com' })`. Creating the engine makes no request yet.

</Engine>

## 3. Open a PDF

<Engine local>

Give the engine the file's bytes. Here we fetch them from a URL; a file the user picked works
the same way (`await file.arrayBuffer()`).

```ts
const response = await fetch('/invoice.pdf');
const doc = await engine.open({ kind: 'bytes', bytes: await response.arrayBuffer() });
```

</Engine>

<Engine cloud>

Open the document with a token your backend created for this user and this document. The token
also says what the user may do with it.

```ts
const doc = await engine.open({ kind: 'token', token: () => fetchDocumentToken('invoice') });
```

[Authentication](/docs/engine/authentication) shows how your backend creates the token, and the
other ways to open a document.

</Engine>

`doc` is your handle to the open document. Everything else starts from it.

## 4. Show the first page

List the pages, then render the first one as an image:

```ts
const { pages } = await doc.pages.list();
const firstPage = doc.page(pages[0].ref);

const image = await firstPage.render.image({ viewport: { kind: 'width', width: 800 } });
const { url } = await image.objectUrl();

document.querySelector('img')!.src = url;
```

Each page has a `ref`: its address in the document. `doc.page(ref)` gives you a handle for that
page, and you pass the same `ref` to anything that works on pages.

## 5. Read the text

```ts
const { text } = await firstPage.text.get();
console.log(text);
```

## 6. Search

Search the whole document. Each match says which page it's on and where it is:

```ts
const { matches } = await doc.search.query({ text: 'total' });

console.log(`${matches.length} matches`);
```

## 7. Highlight a match

A match carries one `segment` per line it covers, each with the four corners of that piece of
text. A highlight takes exactly those corners:

```ts
const match = matches[0];

await doc.page(match.page).annotations.create({
  subtype: 'highlight',
  quadPoints: match.segments.map((segment) => segment.quad),
  color: '#ffd700',
});
```

Render the page again and the highlight is there. See
[Highlights & text markup](/docs/engine/annotations/text-markup) for underlines, strikeouts and
the other fields.

## 8. Save

`download()` returns the PDF with your changes, as bytes:

```ts
const bytes = await doc.download();
```

<Engine local>

To let the user save it, wrap the bytes in a `Blob`:

```ts
const link = document.createElement('a');
link.href = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));
link.download = 'invoice-highlighted.pdf';
link.click();
```

</Engine>

<Engine cloud>

You don't need to save to keep the highlight: the server stored it when you created it.
`download()` is for when you want the file itself.

</Engine>

## 9. Clean up

Close the document when you're done with it. Keep the engine for the next one.

```ts
await doc.close();
```

<Callout type="info">
  Every call returns a promise you can cancel: keep it and call <code>.abort()</code>, for example
  when the user scrolls past a page before its image arrives. See [Errors &
  cancelling](/docs/engine/concepts/errors-and-cancelling).
</Callout>

## Next steps

<Cards>
  <Card
    title="Setup"
    description="Bundlers, workers, server-side rendering and fonts."
    href="/docs/engine/setup"
  />
  <Card
    title="Annotations"
    description="Every kind of annotation, and how to change them."
    href="/docs/engine/annotations"
  />
  <Card
    title="Selection & geometry"
    description="Find the text under the pointer and draw your own selection."
    href="/docs/engine/text/selection-and-geometry"
  />
</Cards>

---

### Content Engine Setup.Cloud (`docs/content/engine/setup.cloud.mdx`)

---
title: Setup
description: Install the CloudPDF engine and point it at your CloudPDF server.
---

import { Callout } from '@/components/docs/callout';

# Setup

```sh npm2yarn
npm install @cloudpdf/engine
```

The engine talks to a CloudPDF server over HTTPS: the managed CloudPDF service, or
[your own server](/docs/server/getting-started). It runs in every modern browser and in Node 18 or newer.

## Create the engine once

Create one engine when your app starts and share it everywhere:

```ts
// pdf.ts
import { cloudEngine } from '@cloudpdf/engine';

export const engine = cloudEngine();
```

It connects to CloudPDF SaaS; your own server goes in `baseUrl` (see Options). Creating the engine
makes no request. One engine holds any number of open documents, each with
its own token. Close each document when you're done with it (`doc.close()`), and keep the engine
for the life of your app.

## Tokens

Every request carries a token that says who the user is and what they may do. Your backend
creates it; the browser never sees your secret key. Most apps pass the token when they open a
document:

```ts
const doc = await engine.open({ kind: 'token', token: () => fetchDocumentToken(documentId) });
```

Pass a function rather than a string: the engine calls it for every request, so when your
backend hands out short-lived tokens, a long session keeps working. [Authentication](/docs/engine/authentication) covers the three ways
to open a document and what goes in a token.

## Options

| Option    | What it does                                                                       |
| :-------- | :--------------------------------------------------------------------------------- |
| `baseUrl` | Your own CloudPDF server. CloudPDF SaaS when left out.                             |
| `token`   | A token for every document this engine opens, when you don't pass one to `open()`. |
| `fetch`   | Your own `fetch`, for example to add headers or to test.                           |
| `onRetry` | Called when the engine retries a request the server was too busy for (see below).  |

## Busy servers

When the server is busy or restarting, it tells the engine to try again. The engine waits as
long as the server asks and retries, up to twice, before the call fails. You don't need to do
anything; `onRetry` lets you see it happen:

```ts
const engine = cloudEngine({
  onRetry: ({ path, code, attempt, waitMs }) => {
    console.debug(`retrying ${path} after ${code} (attempt ${attempt}, waited ${waitMs} ms)`);
  },
});
```

Only requests the server didn't apply are retried, so a retry never makes a change twice.

<Callout type="info">
  Building a viewer interface? The [headless documentation](/docs/headless/quick-start) wires
  the engine to pages, selection and annotation tools for you.
</Callout>

---

### Content Engine Setup.Local (`docs/content/engine/setup.local.mdx`)

---
title: Setup
description: Install the local engine, create it once for your app, and run it in any bundler, with server-side rendering, or in Node.
---

import { Callout } from '@/components/docs/callout';

# Setup

```sh npm2yarn
npm install @embedpdf/engine
```

## Create the engine once

Create one engine when your app starts and share it everywhere:

```ts
// pdf.ts
import { localEngine } from '@embedpdf/engine';

export const engine = localEngine();
```

`localEngine()` returns right away and does no work yet. The first `open()` starts PDFium in a
Web Worker; calls you make while it starts wait for it, so you never check whether it's ready.
To start it early, for example while the rest of your app loads, call `engine.warmup()`.

One engine holds any number of open documents. Close each document when you're done with it
(`doc.close()`), and keep the engine for the life of your app.

## Bundlers

The engine's worker and its WebAssembly file ship inside the package. Vite, webpack 5 (and
Next.js), Rollup and Parcel pick them up without any configuration: there's nothing to copy and
nothing is fetched from a CDN.

If your setup can't bundle a worker (a strict Content Security Policy, an older bundler), host
the files yourself: [Self-hosting](/docs/viewer/self-hosting) lists every option.

## Server-side rendering

A module-level `localEngine()` is safe in Next.js, Nuxt and SvelteKit: it does nothing until you
open a document, and that only happens in the browser. In the Next.js App Router, mark the
component that opens documents with `'use client'`.

## Node.js

The same call works in Node. There the engine runs PDFium in the same process, natively when
your platform has a build and as WebAssembly otherwise:

```ts
import { readFile, writeFile } from 'node:fs/promises';
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
const doc = await engine.open({ kind: 'bytes', bytes: await readFile('in.pdf') });

// …change the document…

await writeFile('out.pdf', await doc.download());
await doc.close();
await engine.destroy();
```

In a script, destroy the engine at the end so the process can exit.

Rendering works in Node without a canvas: `render.image()` gives you PNG or BMP bytes. WebP needs a
canvas, so in Node pass your own `imageEncoder` (for example one built on `sharp`) if you need it.
To pick the runtime yourself, pass `runtime: { prefer: 'wasm' }` or `{ prefer: 'native' }`.

## Fonts

Text the engine draws (a free text annotation, a filled form field) needs a font that has its
characters. For languages the built-in fonts don't cover, give the engine a fallback font when you
create it:

```ts
const engine = localEngine({
  fallbackFonts: [{ key: 'noto-cjk', familyName: 'Noto Sans CJK', url: '/fonts/NotoSansCJK.ttf' }],
});
```

The font is loaded while the engine starts, so it's ready before the first document opens.
[Fonts](/docs/engine/documents/fonts) covers registering fonts later and using them by name.

## With a viewer

The viewer can use your engine or create its own:

- `<Viewer engine={engine} />` uses your engine and never destroys it. Share one engine across
  every viewer in your app.
- `<Viewer engine={() => localEngine()} />` lets the viewer create an engine when it mounts and
  destroy it when it unmounts.

<Callout type="info">
  Building your own viewer interface? The [headless documentation](/docs/headless/quick-start)
  wires the engine to pages, selection and annotation tools for you.
</Callout>

## Options

| Option                 | What it does                                                                                                                                     |
| :--------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- |
| `fallbackFonts`        | Fonts used for characters the document's own fonts don't have.                                                                                   |
| `fonts`                | Fonts you use by name only (a free text `fontFamily`), not as fallback.                                                                          |
| `worker`               | Your own worker: a `Worker`, a function returning one, a URL, or `'inline'`.                                                                     |
| `wasmUrl`              | Where to load the WebAssembly file from, when you host it yourself.                                                                              |
| `renderPolicy`         | The render sizes documents answer with; see [Render policy](/docs/engine/documents/rendering#render-policy).                                     |
| `imageEncoder`         | Your own image encoder, for formats the engine can't encode where it runs (WebP in Node).                                                        |
| `signedDocumentPolicy` | `'permit'` stops the engine refusing what signatures forbid; see [Turning protection off](/docs/engine/forms/signatures#turning-protection-off). |
| `runtime`              | In Node: `{ prefer: 'native' }` or `{ prefer: 'wasm' }`. The engine picks native when it can.                                                    |

---

### Content Headless Errors (`docs/content/headless/errors.mdx`)

---
title: Errors
description: What each numbered error from the Angular package means, and how to fix it.
---

# Errors

<Fw only={['react', 'vue', 'svelte']}>

This page lists the numbered errors of the Angular package; switch the docs to Angular to read it.
In <Framework />, a component used outside the viewer throws an error that says so; see
[Setup](setup) for where the viewer goes.

</Fw>

<Fw only="angular">

When the viewer is set up in a way that can't work, the Angular package throws an error with a
number, such as `EPDF-101`, says how to fix it, and links here. Each section below is one number:
what happened, and the fix.

A document that isn't open yet, or a permission the user doesn't have, isn't a setup mistake: a
plugin refuses those with a `PluginError` (`not-ready`, `permission-denied`), the same in every
framework. See [Permissions](concepts/permissions).

## EPDF-101

**No viewer above it.** A service of the viewer (`inject(EpdfDocuments)`, `inject(EpdfViewer)`)
or one of its components (`<epdf-stage>`, `*epdfDocumentGate`) found no `provideEmbedPdf()` in
the component that uses it or in any component above it.

Add `provideEmbedPdf()` to the `providers` of the component that shows the viewer. That component
and everything in its template share the viewer, and the component can inject it itself:

<Snippet name="errors/provide-viewer" />

For one viewer in the whole app, put the same call in `app.config.ts`; for one per route, in the
route's `providers`. A component outside the one that provides the viewer, such as a sibling in
your layout, can't reach it: move `provideEmbedPdf()` up to a component both are inside.

## EPDF-102

**No viewer with this plugin.** A plugin's service (`inject(EpdfRender)`, `inject(EpdfSearch)`)
found no viewer above it that has the plugin. Either there's no viewer above it at all, or the
viewer's `provideEmbedPdf()` doesn't have the plugin's feature.

Add the feature the error names to `provideEmbedPdf()`, in the component that provides the viewer
or in your app config. `withRender()` gives `EpdfRender`, `withSearch()` gives `EpdfSearch`, and
so on:

<Snippet name="errors/add-feature" />

## EPDF-103

**A layer outside a page.** Something that draws on a page, such as `<epdf-render-layer>`, isn't
inside a page. A layer needs the page it draws on, and only the Stage gives one.

Put it inside `<ng-template epdfPage>` in an `<epdf-stage>`. The Stage draws that template once
for every page on screen:

<Snippet name="errors/layer-in-page" />

## EPDF-104

**Part of a Stage outside one.** UI that belongs to a Stage, such as `<epdf-scrollbar>` or
`<epdf-anchored>`, isn't inside an `<epdf-stage>`. It scrolls that Stage, or follows its pages,
so it needs to be in it.

Put it between the Stage's tags, next to the page template. It sits over the pages, in the Stage's
own box:

<Snippet name="errors/inside-stage" />

## EPDF-105

**An engine call on the server.** Code called a method that needs the engine (`open()`, a page's
picture, a zoom) while Angular rendered the page on the server. The engine only runs in the
browser, so on the server the viewer renders its empty state and every such call throws.

Call it from something that only happens in the browser: an event handler (`(click)`), or
`afterNextRender()` for work when the component first shows:

<Snippet name="errors/browser-only" />

To keep the viewer out of the server render altogether, put it in a `@defer (on viewport)`
block. See [Setup](setup#server-side-rendering).

</Fw>

---

### Content Headless Quick Start (`docs/content/headless/quick-start.mdx`)

---
title: Quick start
description: Build a PDF viewer step by step, with text selection, zoom buttons, a highlighter and a save button.
---

import { Cards, Card } from '@/components/docs/cards';

# Quick start

This guide builds a small PDF viewer: the pages, text people can select, zoom buttons, a
highlighter and a save button. Each step adds one plugin or one control, so you can stop at any
step and have a working viewer.

## 1. Install

You need two packages: the one for your framework, and an engine.

<Fw only="react">
<Engine local>
```sh npm2yarn
npm install @embedpdf/react @embedpdf/engine
```
</Engine>
<Engine cloud>
```sh npm2yarn
npm install @embedpdf/react @cloudpdf/engine
```
</Engine>
</Fw>

<Fw only="vue">
<Engine local>
```sh npm2yarn
npm install @embedpdf/vue @embedpdf/engine
```
</Engine>
<Engine cloud>
```sh npm2yarn
npm install @embedpdf/vue @cloudpdf/engine
```
</Engine>
</Fw>

<Fw only="svelte">
<Engine local>
```sh npm2yarn
npm install @embedpdf/svelte @embedpdf/engine
```
</Engine>
<Engine cloud>
```sh npm2yarn
npm install @embedpdf/svelte @cloudpdf/engine
```
</Engine>
</Fw>

<Fw only="angular">
<Engine local>
```sh npm2yarn
npm install @embedpdf/angular @embedpdf/engine
```
</Engine>
<Engine cloud>
```sh npm2yarn
npm install @embedpdf/angular @cloudpdf/engine
```
</Engine>
</Fw>

Every feature is a separate import from the framework package, such as `@embedpdf/react/stage`,
so what you don't use never reaches your bundle.

## 2. Show a PDF

Create the engine once, then give it to `<Viewer>` with the plugins you want and the document to
open. `<Stage>` shows the pages, and `<RenderLayer>` draws each one:

<Example name="getting-started/basic" mode="open" />

<Engine local>

<Fw only={['react', 'vue', 'svelte']}>

`localEngine()` costs nothing until it's used, so creating it at the top of a module is safe, even
with server-side rendering. The viewer starts it when it mounts: PDFium boots in a Web Worker in
the background, and the first page shows as soon as the document is open. The engine's
WebAssembly is part of your build and loads from your own site; see [Setup](setup).

</Fw>

<Fw only="angular">

`localEngine()` costs nothing until it's used, so it's safe in your component, even with
server-side rendering. The viewer starts it in the browser: PDFium boots in a Web Worker in the
background, and the first page shows as soon as the document is open. The engine's WebAssembly is
part of your build and loads from your own site; see [Setup](setup).

</Fw>

</Engine>

<Engine cloud>

`cloudEngine()` holds an HTTP client and nothing else, so creating it at the top of a
module is safe, even with server-side rendering. Every page, text and annotation call goes to your
CloudPDF server; see [Setup](setup).

</Engine>

`<DocumentGate>` shows its `fallback` until the document is open, so the Stage never renders
without one.

## 3. Let people select text

Add the interaction plugin, which handles the pointer for every feature, and the selection plugin.
Then put `<SelectionLayer>` above the page:

<Snippet name="start/select-text" />

Drag over text to select it, double-click a word, or triple-click a line. On a phone, long-press a
word. See [Selecting & copying](text/selection) for copying and a selection menu.

## 4. Add zoom buttons

<Fw only={['react', 'vue', 'svelte']}>

Every plugin has a <Word of="hook" /> for its API and one for its state. `useStage()` does things,
and `useStageState()` gives you what to show:

</Fw>

<Fw only="angular">

Every plugin has a service with its methods and its state as signals, and the Stage has
`EpdfStageRef`. Next to the Stage in a template, its template reference, `#stage="epdfStage"`,
gives you both:

</Fw>

<Snippet name="start/zoom-controls" />

<Fw only={['react', 'vue', 'svelte']}>

Put `<ZoomControls />` anywhere inside `<DocumentGate>`. It <Word of="rerenders" /> when the zoom
changes, whether a button, a pinch or code changed it.

</Fw>

<Fw only="angular">

The buttons go inside `*epdfDocumentGate`, next to the stage they name. The percentage updates when
the zoom changes, whether a button, a pinch or code changed it.

</Fw>

## 5. Add a highlighter

The annotation plugin shows the annotations a PDF already has, and lets people add their own. Add
it with its layer, and a button that turns on the highlight tool:

<Snippet name="start/highlighter" />

With the tool on, select text and it's highlighted. Click a highlight with the `pointer` tool to
select it. The page picture leaves the annotations out by itself, because the annotation layer
draws them now. <Fw only="angular">The component that provides the viewer can inject its services
too, so the button needs no component of its own. </Fw>See [Annotations](annotations) for every
tool.

## 6. Save

`documents.download()` gives you the PDF with every change, as bytes. `saveFile` hands them to the
browser as a download:

<Snippet name="start/save-button" />

<Fw only={['react', 'vue', 'svelte']}>Put `<SaveButton />` anywhere inside `<Viewer>`.</Fw>
<Fw only="angular">Put `<app-save-button />` anywhere in the template of the component that provides
the viewer: it injects the service from there.</Fw>

<Engine cloud>

You don't need to save to keep the highlights: the server stored each one as it was made.
`download()` is for when you want the file itself.

</Engine>

## Next steps

<Cards>
  <Card
    title="Setup"
    description="Server-side rendering, bundlers, fonts, and hosting the engine yourself."
    href="setup"
  />
  <Card
    title="How it works"
    description="Plugins, how your UI talks to them, layers, and which document a component talks to."
    href="concepts/how-it-works"
  />
  <Card
    title="Scroll, zoom & layout"
    description="Layouts, zoom modes, page navigation and touch."
    href="viewing/stage"
  />
</Cards>

---

### Content Headless Setup (`docs/content/headless/setup.mdx`)

---
title: Setup
description: Install the headless packages for your framework, create the engine once, and use them with server-side rendering.
---

import { Callout } from '@/components/docs/callout';

# Setup

A headless viewer needs two packages: the one for your framework, and an engine. This page
installs them, creates the engine for your whole app, and covers server-side rendering, bundlers
and fonts.

## Install

<Fw only="react">
<Engine local>
```sh npm2yarn
npm install @embedpdf/react @embedpdf/engine
```
</Engine>
<Engine cloud>
```sh npm2yarn
npm install @embedpdf/react @cloudpdf/engine
```
</Engine>
</Fw>

<Fw only="vue">
<Engine local>
```sh npm2yarn
npm install @embedpdf/vue @embedpdf/engine
```
</Engine>
<Engine cloud>
```sh npm2yarn
npm install @embedpdf/vue @cloudpdf/engine
```
</Engine>
</Fw>

<Fw only="svelte">
<Engine local>
```sh npm2yarn
npm install @embedpdf/svelte @embedpdf/engine
```
</Engine>
<Engine cloud>
```sh npm2yarn
npm install @embedpdf/svelte @cloudpdf/engine
```
</Engine>
</Fw>

<Fw only="angular">
<Engine local>
```sh npm2yarn
npm install @embedpdf/angular @embedpdf/engine
```
</Engine>
<Engine cloud>
```sh npm2yarn
npm install @embedpdf/angular @cloudpdf/engine
```
</Engine>
</Fw>

The framework package holds every plugin, one import path each (`@embedpdf/react/search`,
`@embedpdf/react/annotation`). A plugin you don't import isn't in your bundle.

<Fw only="react">

It works with React 18 and later.

</Fw>

<Fw only="vue">

It's written for Vue 3: components for your templates and composables for `<script setup>`.

</Fw>

<Fw only="svelte">

It's written for Svelte 5: components that take snippets, and functions that give you reactive
state for your runes.

</Fw>

<Fw only="angular">

It needs Angular 20.2 or later. Everything is standalone, `OnPush` and built on signals, so it works
in zoneless apps (`provideZonelessChangeDetection()`), the default for new ones.

</Fw>

## Create the engine once

Create one engine when your app starts, and share it with every viewer:

<Engine local>

```ts
// pdf.ts
import { localEngine } from '@embedpdf/engine';

export const engine = localEngine();
```

Creating the engine is instant and allocates nothing. The first viewer that uses it starts it, and
PDFium boots in a Web Worker in the background.

</Engine>

<Engine cloud>

```ts
// pdf.ts
import { cloudEngine } from '@cloudpdf/engine';

export const engine = cloudEngine();
```

It connects to CloudPDF SaaS. Your own server names itself:
`cloudEngine({ baseUrl: 'https://pdf.example.com' })`. Creating the engine makes no request.

</Engine>

<Fw only={['react', 'vue', 'svelte']}>

`<Viewer>` can also own the engine. Pass a function, and the viewer creates the engine when it
mounts and destroys it when it unmounts:

</Fw>

<Fw only="angular">

`provideEmbedPdf()` can also own the engine. Pass a function: the viewer calls it in the browser
when it starts, and destroys the engine when the component or route that provides it goes away:

</Fw>

<Engine local>

<Snippet name="start/owned-engine" />

<Fw only="angular">

The function may return a promise. With `import()`, as here, the engine loads in its own chunk the
first time a viewer needs it, instead of with your page.

</Fw>

</Engine>

<Engine cloud>

<Snippet name="start/owned-engine-cloud" />

</Engine>

An engine you pass in yourself is never destroyed by the viewer, so one engine can serve every
viewer in your app.

## Register the plugins

<Fw only={['react', 'vue', 'svelte']}>

Plugins are a list you create once, next to the engine:

</Fw>

<Fw only="angular">

Each plugin is a feature of `provideEmbedPdf()`, given with the engine. Put it in the `providers`
of the component that shows the viewer:

</Fw>

<Snippet name="start/plugins" />

<Fw only="react">

The viewer reads `engine` and `plugins` once, when it mounts. Creating the list inside a component
would make a new list on every render; the viewer ignores it and warns you while you develop.

</Fw>

<Fw only={['vue', 'svelte']}>

The viewer reads `engine` and `plugins` once, when it mounts, and ignores later changes to them.

</Fw>

<Fw only="angular">

That component and everything in its template share one viewer, and the component can `inject()`
the plugins' services itself. In `app.config.ts` or a route's `providers`, the same call gives the
whole app or route one viewer, so a toolbar in your header and the pages in your outlet work
together. The viewer reads the engine and the features once, when it starts.

A setup that can't work, such as a service with no viewer above it, throws an error with a
number and its fix, like Angular's own. [Errors](errors) lists them.

</Fw>

Order doesn't matter, and a plugin that needs another says so when the app starts: the annotation
plugin, for example, needs the interaction plugin.

## Server-side rendering

The headless packages render nothing on the server that needs the engine: the engine only starts in
the browser. So a module-level engine is safe everywhere.

<Fw only="react">

In the Next.js App Router, mark the component that renders `<Viewer>` as a client component:

</Fw>

<Fw only="vue">

In Nuxt, render the viewer on the client only, inside `<ClientOnly>`. The server renders its
`#fallback` slot instead:

</Fw>

<Fw only="svelte">

In SvelteKit, render the viewer in the browser only, for example behind `{#if browser}` with
`browser` from `$app/environment`:

</Fw>

<Fw only="angular">

With Angular SSR, the viewer works as it is, and the server renders the page around it. To keep its
code, the engine included, out of the first download, put it in a `@defer (on viewport)` block: it
loads when it scrolls into view.

</Fw>

<Snippet name="start/ssr" />

<Fw only="vue">

A component named `DocumentViewer.client.vue` renders on the client only wherever you use it, with
no `<ClientOnly>` around it.

</Fw>

<Fw only="svelte">

To leave a whole page out of server rendering, add `export const ssr = false` to its `+page.ts`.

</Fw>

<Fw only="angular">

The server renders the `@placeholder`, and the deferred component loads in its own chunk with
everything it imports. `on viewport` watches the placeholder, so give it the viewer's height, and
nothing moves when the viewer takes its place.

</Fw>

<Engine local>

## Bundlers and self-hosting

The engine's worker and WebAssembly ship inside the package, and your build picks them up.

<Fw only="react">

Vite, webpack 5 and Rspack need no configuration, and neither does Next.js, with webpack or
Turbopack.

</Fw>

<Fw only="vue">

Vite, and so Nuxt, needs no configuration, and neither do webpack 5 and Rspack.

</Fw>

<Fw only="svelte">

Vite, and so SvelteKit, needs no configuration, and neither do webpack 5 and Rspack.

</Fw>

<Fw only="angular">

The Angular CLI needs no configuration. Its application builder can't copy the WebAssembly as a
file, so `@embedpdf/engine` gives it its portable build without being asked: the WebAssembly
travels inside your build as a lazy chunk and is unpacked in the browser, a moment's work when the
engine starts.

</Fw>

There's nothing to copy, and nothing is fetched from a CDN. The engine loads from your own site, so
it works offline and behind a firewall.

If your setup can't bundle a worker, for example with a strict Content Security Policy, host the
files yourself: [Self-hosting](/docs/viewer/self-hosting) lists every option, with notes for Parcel
and plain esbuild.

## Fonts

Text the engine draws, such as a text box or a filled form field, needs a font with its characters.
For languages the built-in fonts don't cover, give the engine a fallback font:

```ts
export const engine = localEngine({
  fallbackFonts: [{ key: 'noto-cjk', familyName: 'Noto Sans CJK', url: '/fonts/NotoSansCJK.ttf' }],
});
```

[Fonts](/docs/engine/documents/fonts) covers registering fonts and using them by name. Every other
engine option is in the engine's [Setup](/docs/engine/setup#options).

<CloudPdfCallout
  title="The same viewer, on your server"
  href="https://www.cloudpdf.com/docs/headless/setup"
  cta="Set up CloudPDF"
>

Move a headless viewer to CloudPDF by changing one line: `localEngine()` becomes
`cloudEngine()`. Your plugins, components and <Word of="hooks" /> stay as they are.
Files stay on your server, the first page shows without downloading the PDF, and fonts, limits and
storage are the server's to configure.

</CloudPdfCallout>

</Engine>

<Engine cloud>

## Tokens

Each document opens with a token your backend creates: it says who the user is and what they may
do. Pass a function, so a long session gets fresh tokens:

<Snippet name="start/tokens" />

<Fw only="angular">

Providers can't read a component's inputs, so a document that depends on one goes to
`<epdf-stage [document]>`. When the id changes, the viewer opens the new document and closes the
old one.

</Fw>

[Authentication](/docs/engine/authentication) covers the three ways to open a document and what a
token carries. Fonts, limits and storage are your server's configuration.

</Engine>

## Good to know

### One viewer, many documents

A viewer can hold several open documents at once, like tabs. See
[Several documents](documents/tabs).

### Changing `engine` or `plugins`

<Fw only={['react', 'vue']}>

Both are read once. To use another engine or another set of plugins, mount a new `<Viewer>`, for
example by giving it a new `key`.

</Fw>

<Fw only="svelte">

Both are read once. To use another engine or another set of plugins, mount a new `<Viewer>`, for
example inside a `{#key}` block.

</Fw>

<Fw only="angular">

Both are read once, when the viewer starts. To use another engine or other plugins, create the
component that provides them again, for example under another route or an `@if`.

</Fw>

<Callout type="info">
  The ready-made [Viewer](/docs/viewer) uses these same packages. If you start there, everything on
  these pages still applies when you move to headless.
</Callout>

---

### Content Viewer Documents (`docs/content/viewer/documents.mdx`)

---
title: Documents
description: Open a PDF, or several as tabs, handle passwords, and download, print or save to your server.
---

# Documents

<Engine local>

Give the viewer a document with `src`: a URL, a `File`, or bytes.

```js
{ src: '/reports/2026-q3.pdf' }
```

A relative URL is relative to your page. A URL on another site needs CORS headers that allow
yours, like any `fetch`.

## Open the file someone picks

</Engine>

<Engine cloud>

Give the viewer a document with `src`. Every document lives on CloudPDF, and `src` says which one
and who opens it:

| `src` | What it is |
| :-- | :-- |
| `{ token }` | A token your backend creates for the user and one document. It says who they are and what they may do. |
| `{ id }` | A document by its id, when the engine's own token allows it (`engine: { token }`) |
| `{ shareToken }` | A share link from the CloudPDF dashboard (`shr_…`), which needs no backend |

```js
{ src: { token: () => fetchDocumentToken('contract-42') } }
```

A token is a string, or a function that gives a fresh one when the old one runs out. The pages come
from CloudPDF, so there's no file to serve and no CORS to set up; a share link opens only on the
websites it allows. [Authentication](/docs/engine/authentication) has what a token carries.

## Open the document someone picks

</Engine>

`src` follows its prop: give it another document, and the viewer opens that one in its place.

<Snippet name="viewer/documents/pick-file" />

<Engine local>

People can also open files themselves: drop a PDF on the viewer, or use **Open** in the document
menu. Both open it as a new tab.

</Engine>

<Engine cloud>

On CloudPDF, every document comes from your server, so the viewer has no **Open** and takes no
dropped files.

</Engine>

## Several documents

Give `documents` instead of `src` to open several at once. Each one gets a tab:

<Engine local>

```js
{
  documents: [
    { src: '/contract.pdf', name: 'Contract' },
    { src: '/appendix.pdf', name: 'Appendix A' },
    { src: '/scan.pdf', name: 'Signed scan', active: true },
  ],
}
```

| Field | What it is | Default |
| :-- | :-- | :-- |
| `src` | A URL, a `File`, or bytes | none |
| `name` | What its tab shows | the file's name |
| `password` | Its password, if it has one | none |
| `active` | Show this one first | the first document |

</Engine>

<Engine cloud>

```js
{
  documents: [
    { src: { token: contractToken }, name: 'Contract' },
    { src: { token: appendixToken }, name: 'Appendix A' },
    { src: { token: scanToken }, name: 'Signed scan', active: true },
  ],
}
```

| Field | What it is | Default |
| :-- | :-- | :-- |
| `src` | `{ token }`, `{ id }` or `{ shareToken }` | none |
| `name` | What its tab shows | `Document` and the start of its id |
| `password` | The PDF's password, if it has one | none |
| `active` | Show this one first | the first document |

</Engine>

The tabs show while more than one document is open. To show them always or never, set
`layout.tabs('always')` or `layout.tabs('never')` in your [layout](customize/layout).
`onDocumentChange` tells you which document is showing each time that changes, and `null` once none is.

## Passwords

A document with a password opens locked, and the viewer asks for it. When you know it, pass it
along and nobody is asked:

<Engine local>

```js
{ documents: [{ src: '/payroll.pdf', password: secret }] }
```

</Engine>

<Engine cloud>

```js
{ documents: [{ src: { token: payrollToken }, password: secret }] }
```

That's the PDF's own password. A share link's passphrase is another one: it goes with the link,
`src: { shareToken, sharePassword }`.

</Engine>

A wrong password asks again. A document that can't be opened at all shows why in its tab, with a
**Try again** button.

## Open and close documents from code

`viewer.documents` opens, closes and switches documents. It's the documents plugin, the same as
[in headless](/docs/headless/documents/opening):

<Engine local>

```js
const { document } = await viewer.documents.open(
  { kind: 'url', url: '/invoice-1042.pdf' },
  { name: 'Invoice 1042' },
);

viewer.documents.setActive(document.id);
viewer.documents.close(document.id);
```

</Engine>

<Engine cloud>

```js
const { document } = await viewer.documents.open(
  { kind: 'token', token: () => fetchDocumentToken('invoice-1042') },
  { name: 'Invoice 1042' },
);

viewer.documents.setActive(document.id);
viewer.documents.close(document.id);
```

`open()` takes a document as the engine names it: `{ kind: 'token' }`, `{ kind: 'id' }` or
`{ kind: 'share' }`, as in [Authentication](/docs/engine/authentication).

</Engine>

`open()` resolves once the document is ready, and makes it the active one.

## Download and print

The document menu has **Download** and **Print**, and `Ctrl`/`Cmd`+`S` and `Ctrl`/`Cmd`+`P` work
while the viewer has focus. A download includes every annotation, form value and signature.

**Print** opens a dialog: every page, the current page or pages such as `1-3, 5`, and whether
annotations print. Nothing is made while the user chooses: its **Print** button prepares those
pages, shows how far it is, and opens the browser's print dialog when they're ready. Safari prints
only from a tap, so there the dialog says the pages are ready and prints from the next tap on
**Print**. Pages print as the PDF says they print: an annotation or a form field that is
set not to print stays off the paper. On a desktop browser with its own PDF viewer, a document that
allows high-quality printing and downloading prints as a PDF, with sharp text and lines. Everywhere
else the pages print as pictures, at 300 DPI, or at 150 DPI when the document allows only
low-resolution printing; the dialog says which.

When the scope doesn't allow printing, or you [turn it off](features/turning-off), **Print** isn't
there, and `Ctrl`/`Cmd`+`P` says so instead of printing the web page.

To run them from your own button, use the commands `document:download` and `document:print`, or
call them from code. The command opens the dialog; `documents.print()` prints every page without
it:

```js
viewer.commands.execute('document:print');
await viewer.documents.print();
```

## Save to your server

`viewer.documents.download()` gives you the PDF with every change, as bytes, without a download
dialog. Make it a button of your own:

```js
{
  commands: [
    {
      id: 'acme:save',
      label: 'Save',
      icon: 'save',
      shortcut: 'Mod+S',
      run: async ({ viewer, documentId }) => {
        const bytes = await viewer.documents.download(documentId);
        await fetch(`/api/documents/${documentId}`, { method: 'PUT', body: bytes });
      },
    },
  ],
  layout: (layout) => layout.replace('document:download', 'acme:save'),
}
```

`Mod+S` now saves to your server instead of downloading, because a command of yours wins over a
built-in one with the same shortcut.

<Engine local>

<CloudPdfCallout
  title="Nothing to save"
  href="https://www.cloudpdf.com/docs/viewer/documents"
  cta="Store changes as they're made"
>

With CloudPDF, every annotation, comment and form value is stored on your server the moment it's
made, and everyone who has the document open sees it. There's no file to upload and nothing to
lose when the tab closes.

</CloudPdfCallout>

</Engine>

## Warn before changes are lost

When someone closes a tab or leaves the page with unsaved changes, the viewer asks first. Unsaved
means changed since it opened or since the last download. `viewer.documents.get().hasUnsavedChanges`
tells you the same, and `onUnsavedChangesChanged` fires when it changes:

```js
viewer.documents.onUnsavedChangesChanged(({ documentId, hasUnsavedChanges }) => {
  saveButton.disabled = !hasUnsavedChanges;
});
```

## Good to know

### Big files

<Engine local>

The local engine needs the whole file before the first page shows, so a large scan takes as long
as its download. The toolbar is there at once; only the pages wait.

</Engine>

<Engine cloud>

CloudPDF renders each page on the server and sends it as an image, so the first page shows at
once, without the browser downloading the whole PDF.

</Engine>

### Where the bytes go

<Engine local>

With the local engine, a document never leaves the browser: no upload, no server. Each document
runs in the engine's worker, so a heavy page doesn't freeze your app.

</Engine>

<Engine cloud>

The file stays on CloudPDF. The browser gets the pages as images, and only what the token allows:
a user who may not download never receives the file, and one who may not copy never receives the
text.

</Engine>

### Every documents method

`viewer.documents` is the headless documents plugin. Its methods, state and events are listed on
[Opening documents](/docs/headless/documents/opening#methods), [Several
documents](/docs/headless/documents/tabs#methods) and [Saving](/docs/headless/documents/saving#methods).

---

### Content Viewer Migrate (`docs/content/viewer/migrate.mdx`)

---
title: Moving from 2.x
description: From EmbedPDF 2.x's viewer to 3.0's, name by name. The new packages, every config key and what replaces it, what 3.0 doesn't have, and what changes for your security policy.
engines: [local]
---

import { Cards, Card } from '@/components/docs/cards';

# Moving from 2.x

3.0's viewer replaces the one in EmbedPDF 2.x: `@embedpdf/snippet`, and the
`@embedpdf/react-pdf-viewer`, `@embedpdf/vue-pdf-viewer` and `@embedpdf/svelte-pdf-viewer`
packages. It's still one tag and one config, but most names changed. Every config key is a prop,
the toolbar is a layout you change in the config, and you reach each plugin by its name. This page
maps each 2.x name to its 3.0 one.

## The packages

| 2.x | 3.0 |
| :-- | :-- |
| `@embedpdf/snippet` | `@embedpdf/viewer` |
| `https://cdn.jsdelivr.net/npm/@embedpdf/snippet@2/dist/embedpdf.js` | `https://cdn.jsdelivr.net/npm/@embedpdf/viewer@3/dist/embedpdf.js` |
| `@embedpdf/react-pdf-viewer` | `@embedpdf/viewer-react` |
| `@embedpdf/vue-pdf-viewer` | `@embedpdf/viewer-vue` |
| `@embedpdf/svelte-pdf-viewer` | `@embedpdf/viewer-svelte` |
| `@embedpdf/snippet`, called from an Angular component | `@embedpdf/viewer-angular` |

Remove the 2.x package, and install the new one:

<Snippet name="viewer/start/install" />

The new package brings the PDF engine and its WebAssembly. If you installed `@embedpdf/engines`,
`@embedpdf/pdfium` or `@embedpdf/models` only for the 2.x viewer, remove them too.

## The viewer

The 2.x code is the comment at the top. The 3.0 code below it does the same:

<Snippet name="viewer/migrate/report" />

- **No `config` prop.** Each key is a prop of its own. A config you keep in an object still works:
  spread it into the viewer, as in the [Quick start](quick-start#3-make-it-yours). In plain HTML,
  `init()` still takes the whole config, without `type: 'container'`, and `target` can be a
  selector.
- **`onReady` gives you the `viewer`**, not the plugin registry. See [From code](#from-code).
- **The viewer is one element, `<embedpdf-viewer>`.** 2.x's framework packages put an
  `<embedpdf-container>` inside a `<div>` of their own. Now there's no `<div>`: your class and style
  go on the viewer itself.
- **Five keys follow their prop:** `src`, `theme`, `locale`, `disable` and `readOnly`. 2.x's
  framework packages read the config once. Every other key is still read once: see
  [Change the config later](setup#change-the-config-later).
- **Server-side rendering needs no wrapper.** Every framework package is safe to import on the
  server, so a `<ClientOnly>` or a dynamic import around the viewer can go. See
  [Setup](setup#server-side-rendering).

<Fw only="angular">

2.x had no Angular package. In 3.0, the viewer is the `EpdfPdfViewer` component,
`<epdf-pdf-viewer>`: each config key is an input, or give them all at once with `[config]`.

</Fw>

## The config, key by key

| 2.x | 3.0 |
| :-- | :-- |
| `src` | `src`: a URL, a `File` or bytes |
| `documentManager.initialDocuments` | `documents`, each with `src`, `name`, `password` and `active`, in place of `url`, `name`, `password` and `autoActivate` |
| `tabBar` | `layout.tabs()` in the [layout](customize/layout): `'always'`, `'multiple'` (the default) or `'never'` |
| `theme` | `theme`, with new keys: see [Theme](#theme) |
| `icons` | `icons`, as path data: see [Buttons and the toolbar](#buttons-and-the-toolbar) |
| `commands.commands` | `commands`, a list |
| `ui.schema` | `layout` |
| `disabledCategories` | `disable`, with new names: see [Turn features off](#turn-features-off) |
| `permissions` | `scope`: see [Permissions](#permissions) |
| `i18n.defaultLocale` | `locale` |
| `i18n.locales` | `strings`: words by language code, with a button's label under its command's id |
| `annotations.annotationAuthor` | `identity: { displayName }` |
| `wasmUrl` | `engine.wasmUrl`. The file is `embedpdf.wasm` now, from `@embedpdf/engine-runtime-wasm32`, in place of `pdfium.wasm`. |
| `fontFallback` | `engine.fallbackFonts`: font files, each with a `key` and a `url` |
| `fonts.ui` | `theme.tokens.font` |
| `fonts.signature` | `plugins.signature.fonts`: font files, each with a `key`, a `label` and a `url` |
| `stamp.manifests` | `plugins.stamp.standard`: `true`, a URL, or `false` for none |
| Every other plugin's key: `zoom`, `scroll`, `annotations`, … | `plugins`, under the 3.0 plugin's name: see [Plugin settings](#plugin-settings) |

### Plugin settings

The plugins' settings are new too. `plugins` takes them under the 3.0 plugin's name, as its page in
the headless docs lists them, with plain strings where 2.x had enums. The ones 2.x apps set most:

| 2.x | 3.0 |
| :-- | :-- |
| `zoom: { defaultZoomLevel: ZoomMode.FitWidth }` | `stage: { zoom: { mode: 'fit-width' } }` |
| `scroll: { defaultStrategy: ScrollStrategy.Horizontal }` | `stage: { layout: 'horizontal' }` |
| `scroll: { defaultPageGap: 20 }` | `stage: { gap: 20 }` |
| `spread: { defaultSpreadMode: SpreadMode.Odd }` | `stage: { spread: 'odd' }` |
| `rotation: { defaultRotation: Rotation.Degree90 }` | `stage: { viewRotation: 90 }` |
| `pan: { defaultMode: 'always' }` | `interaction: { defaultTool: 'pan' }` |
| `thumbnails: { width: 120 }` | `thumbnails: { zoom: { pageWidth: 120 } }` |
| `annotations: { selectAfterCreate: false }` | `annotation: { afterCreate: { select: false } }` |
| `annotations: { deactivateToolAfterCreate: true }` | `annotation: { afterCreate: { tool: 'default' } }` |
| `stamp: { manifests: [] }` | `stamp: { standard: false }` |

All of them in one 3.0 config:

```js
{
  plugins: {
    stage: {
      zoom: { mode: 'fit-width' },
      layout: 'horizontal',
      gap: 20,
      spread: 'odd',
      viewRotation: 90,
    },
    interaction: { defaultTool: 'pan' },
    thumbnails: { zoom: { pageWidth: 120 } },
    annotation: { afterCreate: { select: false, tool: 'default' } },
    stamp: { standard: false },
  },
}
```

[Plugins](code/plugins#every-plugin) links each plugin to its settings.

## From code

<Snippet name="viewer/migrate/print-button" />

`onReady` gives you the [`viewer`](code/viewer) once it's live. `viewer.get('search')` is what
`registry.getPlugin('search').provides()` gave you in 2.x, and `viewer.documents` and
`viewer.commands` need no call at all.

| 2.x | 3.0 |
| :-- | :-- |
| `await container.registry`, or a ref's `registry` | `onReady`. In plain HTML, also `element.viewer` once the element fires `epdf:ready`. |
| `registry.getPlugin(id).provides()` | `viewer.get(name)` |
| `.forDocument(documentId)` on a plugin | `viewer.forDocument(documentId).get(name)` |
| `onInit`, with the container | None: what you did with the container is config now (`theme`, `icons`) |
| `registry.getEngine()` | None: the viewer keeps its engine. Read the document through the plugins instead, such as `viewer.get('metadata')`, and get the file with `viewer.documents.download()`. |

The plugins have new names:

| 2.x plugin | 3.0 plugin |
| :-- | :-- |
| `zoom`, `scroll`, `spread`, `rotate`, `viewport` | `stage` |
| `render`, `tiling` | `render` |
| `thumbnail` | `thumbnails` |
| `bookmark` | `outline`, which also adds, renames, moves and deletes items |
| `attachment` | `attachment`, which also lists the files pinned to pages, and attaches and removes files |
| `document-manager`, `export` | `documents` |
| `interaction-manager`, `pan` | `interaction`, whose `pan` tool is the hand |
| `ui` | `shell` for what's open, and the config's `layout` for what goes where |
| `print`, `fullscreen`, `history` | No plugin: run their commands, `document:print`, `view:fullscreen`, `history:undo` and `history:redo` |
| `annotation`, `search`, `selection`, `form`, `redaction`, `stamp`, `signature`, `i18n`, `commands` | The same name |

Their methods are new as well, each on its plugin's page in the headless docs. The calls 2.x code
makes most:

| 2.x | 3.0 |
| :-- | :-- |
| `openDocumentUrl({ url })` | `viewer.documents.open({ kind: 'url', url })` |
| `openDocumentBuffer({ buffer, name })` | `viewer.documents.open({ kind: 'bytes', bytes: buffer }, { name })` |
| `setActiveDocument(id)`, `closeDocument(id)` | `viewer.documents.setActive(id)`, `viewer.documents.close(id)` |
| `onDocumentOpened(callback)` | `viewer.documents.onOpened(({ documentId }) => …)` |
| `saveAsCopy()` | `viewer.documents.download()`: the PDF's bytes |
| `download()` | `viewer.commands.execute('document:download')` |
| `scrollToPage({ pageNumber: 5 })` | `viewer.get('stage').goToPage(4)`: pages count from 0 |
| `setLocale('de')` | The `locale` prop |

## Theme

2.x set colors per mode, in groups: `theme.light.accent.primary`. 3.0 has one set of tokens for both
modes, and `dark` for what's different in dark mode:

```js
{
  theme: {
    preference: 'system',
    accent: '#9333ea',
    tokens: { canvas: '#f5f3ff' },
    dark: { accent: '#a855f7', canvas: '#1e1b2e' },
  },
}
```

2.x's `light` colors go in `tokens`, and its `dark` ones in `dark`:

| 2.x color | 3.0 token |
| :-- | :-- |
| `accent.primary` | `accent` |
| `background.app` | `canvas` |
| `background.surface` | `surface` |
| `background.elevated` | `surface-raised` |
| `foreground.primary` | `text` |
| `foreground.secondary` | `text-muted` |
| `border.default` | `border` |
| `interactive.hover` | `hover` |

The other 2.x colors, such as `accent.primaryHover` or `state.error`, have no token. For what tokens
don't cover, restyle a part with `::part()`: see [Theming](customize/theming#restyle-a-part).
The container's `setTheme()` is the `theme` prop now, which follows its value.

## Buttons and the toolbar

2.x changed the toolbar after the viewer started: `registerCommand()`, `registerIcon()`, then
`mergeSchema()` with a copy of the toolbar's items. In 3.0 all three are config, and the layout is
a function that gets the default and returns yours. Here is 2.x's example, a smiley in place of the
comments button:

```js
{
  icons: {
    smiley: [
      'M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0',
      'M9 10l.01 0',
      'M15 10l.01 0',
      'M9.5 15a3.5 3.5 0 0 0 5 0',
    ],
  },
  commands: [{ id: 'acme:hello', label: 'Say hello', icon: 'smiley', run: () => alert('Hello!') }],
  layout: (layout) => layout.replace('panel:comments', 'acme:hello'),
}
```

| 2.x | 3.0 |
| :-- | :-- |
| `commands.registerCommand(command)` | `commands: [command]` |
| A command's `action` | `run` |
| `disabled` | `enabled`, which says the opposite |
| `shortcuts` | `shortcut` |
| `registry` and `state`, in a command's functions | `get`, which gives you any plugin by its name |
| The container's `registerIcon(name, { viewBox, paths })` | `icons`: 24 × 24 path data, each path a string |
| `ui.mergeSchema({ toolbars })` | `layout: (layout) => layout.add(…)`, `.remove(…)`, `.replace(…)` |
| `ui.toggleMenu()` in a command | `menu: 'id'` on the command |
| A `{ type: 'custom', componentId }` item | `{ custom: 'id' }`, with [your own component](customize/components) |

A command's `label`, `labelKey`, `icon`, `active`, `visible` and `categories` keep their names.
The ids changed:

| 2.x | 3.0 |
| :-- | :-- |
| `main-toolbar` | `main` |
| `annotation-toolbar`, `shapes-toolbar`, `insert-toolbar`, `form-toolbar`, `redaction-toolbar` | `annotate`, `shapes`, `insert`, `form`, `redact` |
| `document-menu`, `zoom-menu`, `page-settings-menu` | `document`, `zoom`, `view` |
| The `selection` and `annotation` selection menus | The `selection` and `annotation` bars |
| `document:export`, `document:fullscreen` | `document:download`, `view:fullscreen` |
| `annotation:add-highlight`, `panel:toggle-search`, `scroll:next-page` | `tool:highlight`, `panel:search`, `page:next` |

Every 3.0 id is on [Layout](customize/layout#every-built-in-id) and
[Buttons & commands](customize/commands#every-built-in-command).

## Turn features off

`disabledCategories` is `disable`. It takes 3.0's [features](features/turning-off), with their parts
after a dot, and single commands by their id:

| 2.x | 3.0 |
| :-- | :-- |
| `annotation` | `annotations` and `shapes`: 2.x's shapes were annotations |
| `annotation-highlight` | `annotations.highlight` |
| `annotation-ink` | `annotations.drawing` |
| `annotation-shape` | `shapes` |
| `insert-rubber-stamp`, `insert-image` | `stamps` |
| `insert-signature` | `signatures.signing` |
| `form` | `forms` |
| `redaction` | `redaction` |
| `panel-search` | `search` |
| `panel-comment` | `comments` |
| `selection-copy` | `text.copy` |
| `document-open`, `document-export`, `document-print` | `documents.open`, `documents.download`, `documents.print` |
| `zoom`, `rotate`, `spread`, `scroll`, `pan` | No feature: [remove](customize/layout#remove-buttons) their buttons, or disable each command, such as `'zoom:in'` |

New in 3.0, `readOnly: true` turns off everything that changes the document.

## Permissions

2.x's `permissions` changed what the viewer offered. 3.0's `scope` is the list of what the user
[may do](features/turning-off#what-a-user-may-do), and the engine checks it on every call:

| 2.x | 3.0 |
| :-- | :-- |
| No `permissions`: the PDF's own restrictions count | No `scope`: the same |
| `enforceDocumentPermissions: false` | `scope: ['*']`: the user may do everything |
| `overrides: { print: true }` | `'doc.print'` added to the default scope, which [Permissions](/docs/headless/concepts/permissions) lists |
| `overrides: { print: false }` | A scope that leaves out both `'doc.print'` and `'pdf.permissions'`, or, for the button only, `disable: ['documents.print']` |

2.x's `print` is `doc.print` in a scope, `printHighQuality` is `doc.print.high`, `copyContents` is
`doc.text.copy`, `modifyAnnotations` is `doc.annotate.modify`, `fillForms` is `doc.forms.fill` and
`assembleDocument` is `doc.pages.assemble`. See [Permissions](/docs/headless/concepts/permissions).

## Security and self-hosting

### Nothing from a third party

2.x fetched several files from public servers. 3.0 fetches nothing from a third party unless you
point it there:

| 2.x fetched | 3.0 |
| :-- | :-- |
| `pdfium.wasm`, from jsDelivr | `embedpdf.wasm`, a file of your build |
| Fallback fonts, from jsDelivr | None, unless you give `engine.fallbackFonts` |
| The UI font and the signature fonts, from Google Fonts | The system font. The signature fonts are lazy chunks of your build. |
| The standard stamps, from jsDelivr | A lazy chunk of your build |

So a PDF whose text needs a font it doesn't embed, such as Chinese or Japanese, needs a fallback
font you give the engine: see [Fonts](/docs/engine/documents/fonts). Every file and its setting is
on [Self-hosting](self-hosting).

### Content Security Policy

2.x drew its styles from a `<style>` element in its shadow root, so a strict policy needed
`style-src 'unsafe-inline'`, and it started the engine's workers from `blob:` URLs. 3.0 needs no
`'unsafe-inline'` or `'unsafe-eval'` and runs with Trusted Types on. Installed from npm, its workers
are files of your build, under `worker-src 'self'`; from the CDN they still start from `blob:` URLs.
The policy to copy is on [Security & CSP](concepts/security).

### The page around it

- 2.x added its font stylesheets to your page's `<head>`. 3.0 adds nothing to your page: its styles
  stay in its own shadow DOM.
- Menus and tooltips open in the browser's top layer, so your page's headers never cover them,
  while panels and dialogs stay inside the viewer's box. See
  [How it works](concepts/how-it-works#its-own-box).
- 3.0 needs Chrome and Edge 114, Firefox 125 or Safari 17, or newer: see
  [Setup](setup#what-it-needs-from-the-browser).

## Not in 3.0

What 2.x had that 3.0 doesn't:

- **Loading in parts.** 2.x's `mode: 'range-request'` fetched a big file a piece at a time. The
  built-in engine downloads the whole file first: see [Big files](documents#big-files).
- **Request options.** For headers or credentials (`requestOptions`), fetch the file yourself and
  give `src` its bytes or a `File`.
- **A limit on open documents** (`maxDocuments`).
- **The engine on the page's own thread** (`worker: false`), and its log (`log`). The engine
  always runs in a worker.
- **Zoom limits and steps:** `minZoom`, `maxZoom`, `zoomStep`, `zoomRanges` and `presets`.
- **Marquee zoom, and the screenshot tool** (the `capture` plugin).
- **Protect.** 2.x's Protect dialog, `setDocumentEncryption()` and `removeEncryption()`: 3.0 can't
  add or remove a document's password.
- **Swedish and Traditional Chinese.** They aren't built in: add them with
  [`strings`](customize/translations#a-language-of-your-own).
- **Theme events:** `themechange`, `activeColorScheme` and `themePreference`.
- **A few settings:** `export.defaultFileName`, `fullscreen.targetElement`, `i18n.fallbackLocale`
  (English is the fallback) and `i18n.paramResolvers`.

## Next steps

<Cards>
  <Card
    title="Quick start"
    description="Install, show a PDF, add your color and your own button."
    href="quick-start"
  />
  <Card
    title="Layout"
    description="Toolbars at any edge, several of them, and every button where you want it."
    href="customize/layout"
  />
  <Card
    title="The viewer in code"
    description="Open documents, run commands and use any plugin from your app."
    href="code/viewer"
  />
</Cards>

---

### Content Viewer Quick Start (`docs/content/viewer/quick-start.mdx`)

---
title: Quick start
description: Install the viewer, show a PDF, give it your color and your own button, put your own component in the toolbar, and talk to it from code.
---

import { Cards, Card } from '@/components/docs/cards';

# Quick start

This guide adds the viewer to your app, then makes it yours in four small steps. Each step works on
its own, so stop whenever it's enough.

## 1. Install

<Snippet name="viewer/start/install" />

<Engine local>

The package brings everything: the toolbar and panels, the PDF engine, and its WebAssembly, which
your build serves from your own site.

</Engine>

<Engine cloud>

The package brings everything: the toolbar and panels, and the engine that opens your documents on
CloudPDF, which renders the pages. Only Stamps and Signatures load WebAssembly, the first time
someone opens them.

</Engine>

## 2. Show a PDF

Give the viewer a document and a height:

<Snippet name="viewer/start/first" />

<Engine cloud>

`shareToken` is a share link from the CloudPDF dashboard, which needs no backend; this one opens our
demo document. In your app, open each document with a token your backend creates for the user: see
[Documents](documents).

</Engine>

The viewer fills the box you give it, so it needs a height: a fixed one, `100vh`, or the rest of a
flex layout. People can now scroll, zoom, search, select text, annotate, fill in forms, sign and
download.

## 3. Make it yours

The config is plain data, the same in every framework, so keep it in a file of its own. This one
uses your brand color, leaves out two features, and adds a button to the toolbar:

```js filename="viewer-config.js"
export const config = {
  theme: { accent: '#7c3aed' },
  disable: ['forms', 'redaction'],
  icons: {
    send: ['M10 14L21 3', 'M21 3l-6.5 18a.5.5 0 0 1-1 0L10 14l-7-3.5a.5.5 0 0 1 0-1L21 3z'],
  },
  commands: [
    {
      id: 'acme:send',
      label: 'Send for signature',
      icon: 'send',
      run: ({ documentId }) => fetch(`/api/send/${documentId}`, { method: 'POST' }),
    },
  ],
  layout: (layout) => layout.add('acme:send', { to: 'main', section: 'end' }),
};
```

Then give it to the viewer:

<Snippet name="viewer/start/customized" />

<Example name="viewer/quick-start/customized" />

The new button behaves like the built-in ones: it has a tooltip, it moves into the "More" menu when
the toolbar runs out of room, and it's reachable with the keyboard. The icon is 24 × 24 SVG path
data, so [Tabler](https://tabler.io/icons) and [Lucide](https://lucide.dev) icons paste in as they
are.

## 4. Put your own component in the toolbar

A button runs one action. For anything more, such as a status from your app, put your own
component in the toolbar. Make room for it in the layout, then give the viewer the component:

<Snippet name="viewer/start/component" />

Your component stays part of your app: your CSS styles it, and it reads your app's state as it
always does. The viewer only decides where it goes. See [Your own components](customize/components)
for menus, panels and the viewer's look.

## 5. Talk to the viewer

`onReady` gives you the `viewer` once it's live. This one searches each document when it opens:

<Snippet name="viewer/start/ready" />

`viewer.get('search')` is the search plugin, the same one the [headless
docs](/docs/headless/text/search) describe. Every feature of the viewer is a plugin you can reach
this way, by its name. See [The viewer in code](code/viewer).

## Next steps

<Cards>
  <Card
    title="Setup"
    description="Sizing, server-side rendering, the engine, and several viewers on a page."
    href="setup"
  />
  <Card
    title="Layout"
    description="Toolbars at the bottom, on the side, floating, or several at once."
    href="customize/layout"
  />
  <Card
    title="Theming"
    description="Your colors, light and dark, and the look of every part."
    href="customize/theming"
  />
</Cards>

---

### Content Viewer Self Hosting.Cloud (`docs/content/viewer/self-hosting.cloud.mdx`)

---
title: Self-hosting
description: The one server the CloudPDF viewer talks to, your own CloudPDF in its place, and the files that run in the browser.
---

# Self-hosting

The viewer talks to one server: CloudPDF SaaS (`https://engine.cloudpdf.com`), which renders the
pages, finds the text and stores every change. To run your own CloudPDF instead, deploy the
[CloudPDF server](/docs/server/getting-started) and say where it is:

```js
{ engine: { baseUrl: 'https://pdf.example.com' } }
```

Every request then goes to your server, and nothing else changes. Add its origin to `connect-src`
in place of CloudPDF SaaS's: see [Security & CSP](concepts/security).

## What runs in the browser

Reading a document is requests to your CloudPDF and nothing more: no WebAssembly, no Web Worker.
Stamps and Signatures are the exception. A stamp library is a PDF the browser cuts into stamps, so
the first time someone opens one of them, the viewer loads the PDF engine (`embedpdf.wasm` and its
worker) and opens the library in it.

| What | Where it comes from by default | Setting |
| :-- | :-- | :-- |
| Your documents | Your CloudPDF | `engine.baseUrl` |
| The engine for stamps (`embedpdf.wasm`, its worker) | Files of your build, emitted by your bundler; for a plain HTML page, the CDN script's folder | `plugins.stamp.assetEngine` |
| The standard stamps | A lazy JavaScript chunk of your build (`@embedpdf/default-stamps`) | `plugins.stamp.standard` |

`plugins.stamp.assetEngine` gives the viewer an engine of your own for stamps, such as one whose
files you serve from another directory:

```js
import { localEngine } from '@embedpdf/engine';

const config = {
  plugins: {
    stamp: { assetEngine: () => localEngine({ assetsUrl: '/vendor/embedpdf/' }) },
  },
};
```

## The CDN snippet

The snippet, `dist/cloudpdf.js`, carries the engine for stamps in its folder: `embedpdf.wasm` next to
its chunks. Every sibling is referenced by a path relative to the folder, so serving it from
jsDelivr, from your own CDN or from an intranet server is the same: copy the folder, never the file
alone.

---

### Content Viewer Self Hosting (`docs/content/viewer/self-hosting.mdx`)

---
title: Self-hosting & air-gapped deployments
description: Every URL EmbedPDF can request, the setting that controls each, and how to run it with no request leaving your origin.
---

import { Callout } from '@/components/docs/callout';

# Self-hosting & air-gapped deployments

EmbedPDF ships with the product. Nothing it needs at runtime comes from a
public CDN unless you point it there: the PDF engine's WebAssembly binary, its
worker, and the built-in stamp library all travel inside your build or inside
the artifact you serve. This page is the complete inventory — every URL a
deployment can touch and the setting that controls it — so an air-gapped or
strict-CSP deployment is a checklist, not an investigation.

Every engine setting on this page goes in the viewer's `engine` config, such as
`engine: { assetsUrl: '/vendor/embedpdf/' }`. In a headless app, the same
settings are `localEngine()` options.

## The inventory

| What                                  | Where it comes from by default                                                                                                              | Setting                              |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `embedpdf.wasm` (the engine, ~6.4 MB) | Emitted by your bundler as an asset of your build, streamed from your origin. Under Angular, a lazy JavaScript chunk of your build instead. | `engine.assetsUrl`, `engine.wasmUrl`, `engine.wasmBinary` |
| The engine worker                     | A file of your build, next to the WebAssembly, emitted by your bundler. From a CDN or with the engine's portable build (Angular, plain esbuild), a `blob:` URL instead (see below). | `engine.worker`                      |
| The image-encoder worker              | The same, with a main-thread fallback.                                                                                                      | `engine.encoderWorker`               |
| Fonts                                 | None are fetched unless you configure them.                                                                                                 | `engine.fonts`, `engine.fallbackFonts` |
| The standard stamps                   | A lazy JavaScript chunk of your build (`@embedpdf/default-stamps`).                                                                         | `plugins.stamp.standard`             |
| Your documents                        | Whatever you pass as `src` or open yourself.                                                                                                | —                                    |

<Engine cloud>

With the cloud engine the document lives on your CloudPDF deployment or on
CloudPDF SaaS, so where it runs (`engine: { baseUrl }`, CloudPDF SaaS when
left out) is the one network destination the engine itself has — and rendering, text, and search happen there, not in the browser. The
rows above still apply to the parts that run locally: the built-in stamp
library opens in the browser, so the first time someone opens Stamps or
Signatures the viewer loads `embedpdf.wasm` and the engine worker. The CDN
snippet ships the WebAssembly in its folder; npm installs go through your
bundler as above. `plugins.stamp.assetEngine` supplies your own local engine instead.

</Engine>

Verify it yourself: open the browser's network panel, filter out your own
origin, and load a document. The list should be empty. We keep it that way
with a nightly matrix that builds a minimal app under every major bundler and
fails on any request that leaves the app's origin.

## The engine's WebAssembly

With no `engine` settings, the viewer is right in almost every toolchain.
Your bundler sees `new URL('./embedpdf.wasm', import.meta.url)` inside
`@embedpdf/engine-runtime-wasm32`, copies the file into your build output, and
the worker streams and compiles it from your origin. Verified out of the box:
Vite, webpack 5, Rspack, Parcel 2, and Next.js with either webpack or Turbopack.

**Angular** resolves packages with the `es2020` export condition, and
`@embedpdf/engine` routes that condition to its portable build: the same engine, with the binary carried as a lazy chunk of your build instead
of an asset. Nothing to configure. It costs the same bytes over the wire and a
short inflate at boot, and it does not stream-compile. If you want the streamed
asset instead, copy the file with one `angular.json` entry and say where it is:

```json
"assets": [
  { "glob": "embedpdf.wasm", "input": "node_modules/@embedpdf/engine-runtime-wasm32/lib", "output": "/embedpdf" }
]
```

```js
{ engine: { assetsUrl: '/embedpdf/' } }
```

Under pnpm, add `@embedpdf/engine-runtime-wasm32` to your own dependencies so
that path exists; npm and yarn hoist it for you.

**Plain esbuild** emits neither the asset nor the engine's workers, and has no
condition to route on. Use the engine's portable build, as Angular does: the
WebAssembly comes as a lazy chunk of your build, and both workers start from
`blob:` URLs, so the policy needs `worker-src blob:` (see below).

<Fw only="vanilla">

Import the portable entry in place of `@embedpdf/viewer`. It's the same viewer,
with that engine:

```ts
import '@embedpdf/viewer/portable';
```

</Fw>

<Fw only={['react', 'vue', 'svelte', 'angular']}>

Add `@embedpdf/engine` to your dependencies and give the viewer that engine:

```js
import { localEngine } from '@embedpdf/engine/portable';

const config = { engine: () => localEngine() };
```

</Fw>

**Explicit sources** win over every default and never fall back to anything:

```js
{ engine: { assetsUrl: '/vendor/embedpdf/' } } // a directory you serve; embedpdf.wasm is appended
{ engine: { wasmUrl: '/vendor/embedpdf/engine.wasm' } } // the exact file
{ engine: { wasmBinary: bytes } } // bytes you already have, so no request at all
```

If the file is missing at boot, the engine fails with a message that names
these options. It never tries another location on its own.

## Workers and Content-Security-Policy

The engine's worker and the image-encoder worker are files of your build, next to
the WebAssembly, so `worker-src 'self'` covers them. To serve them from somewhere
else, copy `workers/embedpdf-worker.js`, `workers/encoder-worker.js` and
`embedpdf.wasm` into one directory you serve and say where the worker is; it
finds the WebAssembly as its own sibling:

```js
{
  engine: {
    worker: '/vendor/embedpdf/embedpdf-worker.js',
    encoderWorker: '/vendor/embedpdf/encoder-worker.js',
  },
}
```

`encoderWorker: false` encodes images on the main thread instead. With the engine's portable
build (Angular, plain esbuild), both workers start from `blob:` URLs unless you set these two; the
WebAssembly still comes from your build, so the two worker files are all you copy.
WebAssembly itself needs `script-src 'wasm-unsafe-eval'`. The whole policy is
on [Security & CSP](concepts/security).

## The standard stamps

The stamps panel's standard stamps are a lazy chunk of your build, one per
language. To serve a copy of `@embedpdf/default-stamps` yourself, or to ship
none:

```js
{ plugins: { stamp: { standard: 'https://files.example.com/stamps/{locale}/stamps.pdf' } } }
{ plugins: { stamp: { standard: false } } }
```

A URL you give is used exactly as given. Libraries your users create are kept
in their browser's IndexedDB, never sent anywhere.

## The CDN snippet

The snippet, `dist/embedpdf.js`, is the one artifact designed to be loaded
from another origin. Its folder is the unit of delivery: the entry, its
`chunks/`, and `embedpdf.wasm`, `embedpdf-worker.js` and `encoder-worker.js` beside it. Every sibling is referenced by a
path relative to the folder, so serving that folder from jsDelivr, from your
own CDN, or from an intranet server is the same operation, and no URL in the
code names any of them. Copy the folder, never the file alone.

A browser starts a Web Worker only from its own origin, so a page that loads
the folder from another one starts the engine's worker from a `blob:` URL. A
strict policy for a page that loads it from `https://cdn.example`:

```
script-src 'self' https://cdn.example 'wasm-unsafe-eval';
worker-src blob:;
connect-src 'self' https://cdn.example;
```

`connect-src` covers the wasm fetch from the folder; add the origins your
documents come from.

## Toolchain notes

- **Parcel 2** resolves our packages only with `packageExports` enabled in
  your project's `package.json`: `"@parcel/resolver-default": { "packageExports": true }`.
- **Turbopack** and **webpack** in Next.js emit the wasm under `_next/static/media`; no configuration.

<Callout type="info">
  The guarantee behind this page is enforced, not promised: a lint fails the build on any public-CDN
  URL literal in the viewer or engine source, and the nightly bundler matrix fails on any request
  that leaves the app's origin.
</Callout>

---

### Content Viewer Setup (`docs/content/viewer/setup.mdx`)

---
title: Setup
description: Install the viewer, give it a size, pass it config, render on the server, and run several viewers on one page.
---

import { Callout } from '@/components/docs/callout';

# Setup

<Engine local>

One package gives you the whole viewer: the toolbar and panels, the PDF engine, and everything the
engine needs, served from your own site.

</Engine>

<Engine cloud>

One package gives you the whole viewer: the toolbar and panels, and the engine that opens your
documents on CloudPDF.

</Engine>

<Snippet name="viewer/start/install" />

<Engine local>

On a plain HTML page, load the script from jsDelivr as above. In an app with a bundler, install
`@embedpdf/viewer` and `import '@embedpdf/viewer'` once, which defines the `<embedpdf-viewer>`
element.

</Engine>

<Engine cloud>

On a plain HTML page, load the script from jsDelivr as above. In an app with a bundler, install
`@cloudpdf/viewer` and `import '@cloudpdf/viewer'` once, which defines the `<embedpdf-viewer>`
element.

</Engine>

## Give it a size

The viewer fills the box you give it and scrolls the pages inside it, so the box needs a height.
Any height works: a fixed one, `100vh`, or the remaining space of a flex or grid layout
(`flex: 1; min-height: 0`). Its width follows the box too, and the viewer adapts to it, not to the
window: see [Small screens](customize/small-screens).

## Pass it config

Everything the viewer can do is one config object. In a framework, every key is a prop:

<Snippet name="viewer/setup/config" />

<Engine local>

| Key | What it is | Page |
| :-- | :-- | :-- |
| `src` | One document: a URL, a `File`, or bytes | [Documents](documents) |
| `documents` | Several documents, shown as tabs | [Documents](documents#several-documents) |
| `layout` | Where every toolbar, button, menu and panel goes | [Layout](customize/layout) |
| `commands` | Your own actions | [Buttons & commands](customize/commands) |
| `icons` | Your own icons | [Buttons & commands](customize/commands#icons) |
| `theme` | `'light'`, `'dark'`, `'system'`, or your colors | [Theming](customize/theming) |
| `locale` | `'auto'` or a language code | [Translations](customize/translations) |
| `strings` | Your own words, and other languages | [Translations](customize/translations) |
| `disable` | Features, or single buttons, to leave out | [Turn features off](features/turning-off) |
| `readOnly` | Every feature that changes the document off | [Turn features off](features/turning-off#read-only) |
| `scope` | What the user may do, checked by the engine | [Turn features off](features/turning-off#what-a-user-may-do) |
| `identity` | Who the user is: the author of their annotations | [All features](features#who-the-user-is) |
| `notAllowed` | `'hide'` (default) or `'disable'` what the scope doesn't allow | [Turn features off](features/turning-off#what-a-user-may-do) |
| `plugins` | The settings of each feature's plugin | [Plugins](code/plugins#settings) |
| `engine` | Where the engine's files come from, or another engine | [Self-hosting](self-hosting) |
| `onReady` | Called with the `viewer` once it's live | [The viewer in code](code/viewer) |
| `onDocumentChange` | Called with the document that's showing: the first one, each one the user switches to, and `null` after the last one closes | [Documents](documents#several-documents) |
| `onPanelResize` | Called with a panel's id and width when the user resizes it | [Panels](customize/panels#keep-the-width-people-chose) |

</Engine>

<Engine cloud>

| Key | What it is | Page |
| :-- | :-- | :-- |
| `src` | One document: `{ token }`, `{ id }` or `{ shareToken }` | [Documents](documents) |
| `documents` | Several documents, shown as tabs | [Documents](documents#several-documents) |
| `layout` | Where every toolbar, button, menu and panel goes | [Layout](customize/layout) |
| `commands` | Your own actions | [Buttons & commands](customize/commands) |
| `icons` | Your own icons | [Buttons & commands](customize/commands#icons) |
| `theme` | `'light'`, `'dark'`, `'system'`, or your colors | [Theming](customize/theming) |
| `locale` | `'auto'` or a language code | [Translations](customize/translations) |
| `strings` | Your own words, and other languages | [Translations](customize/translations) |
| `disable` | Features, or single buttons, to leave out | [Turn features off](features/turning-off) |
| `readOnly` | Every feature that changes the document off | [Turn features off](features/turning-off#read-only) |
| `notAllowed` | `'hide'` (default) or `'disable'` what the user's token doesn't allow | [Turn features off](features/turning-off#what-a-user-may-do) |
| `plugins` | The settings of each feature's plugin | [Plugins](code/plugins#settings) |
| `engine` | Your own CloudPDF server (`baseUrl`), the engine's own `token`, or another engine | [Self-hosting](self-hosting) |
| `onReady` | Called with the `viewer` once it's live | [The viewer in code](code/viewer) |
| `onDocumentChange` | Called with the document that's showing: the first one, each one the user switches to, and `null` after the last one closes | [Documents](documents#several-documents) |
| `onPanelResize` | Called with a panel's id and width when the user resizes it | [Panels](customize/panels#keep-the-width-people-chose) |

Who the user is and what they may do come from the document's token, so there's no `scope` or
`identity`.

</Engine>

The config is plain data and a few functions, the same in every framework. Keep it in a file of its
own when it grows, and spread it into the viewer, as in the [Quick start](quick-start#3-make-it-yours).

## TypeScript

Every key is typed, including the ids of the built-in buttons and panels, so your editor completes
them. A config in a file of its own gets its types from `satisfies`:

```ts
import type { ViewerConfig } from '@embedpdf/viewer';

export const config = {
  theme: { accent: '#7c3aed' },
  layout: (layout) => layout.place('main', 'bottom'),
} satisfies ViewerConfig;
```

Every framework package exports the same `ViewerConfig` type.

## Server-side rendering

The viewer runs in the browser only, and every framework package is safe to import on the server.
The server renders an empty box of the size you gave it, and the viewer starts when the page loads
in the browser, so there's nothing to wrap:

<Snippet name="viewer/setup/ssr" />

Next.js, Nuxt, SvelteKit, Astro and Angular SSR all work this way. With Angular, `@defer (on
viewport)` also keeps the viewer's code out of the first download until it scrolls into view.

<Engine cloud>

## Open a CloudPDF document

Each document opens with a token your backend creates for the user. It says who they are and what
they may do:

```js
{
  src: { token: await fetchDocumentToken('contract-42') },
}
```

Pass a function to get a fresh token when an old one runs out:
`src: { token: () => fetchDocumentToken('contract-42') }`. See
[Authentication](/docs/engine/authentication) for the ways to open a document and what a token
carries.

</Engine>

## Change the config later

Five keys follow their prop while the viewer runs: `src` opens the new document in place of the
old one; `theme` and `locale` switch the look and the language; `disable` and `readOnly` take tools
away or bring them back. Any plugin's settings change
through the `viewer` (see [The viewer in code](code/viewer)). Everything else is read once, when the
viewer starts. To change it, mount a new viewer:

<Snippet name="viewer/setup/remount" />

A new viewer opens its documents again.

## Several viewers on a page

Each viewer has its own documents, layout, settings and state. Keyboard shortcuts work in the
viewer that has focus, so two viewers never both zoom on one key press.

## Good to know

### What it needs from the browser

Chrome and Edge 114, Firefox 125 and Safari 17 (iOS 17), or newer: the first versions with the
popover API, which the viewer's menus and dialogs open in.

<Engine local>

The engine runs in a Web Worker, as WebAssembly.

</Engine>

<Engine cloud>

The pages render on CloudPDF. Stamps and Signatures open their libraries in the browser, in a Web
Worker, as WebAssembly.

</Engine>

### A strict Content Security Policy

The viewer runs under a strict CSP with no `'unsafe-inline'`. [Security & CSP](concepts/security)
has the policy to copy.

### Air-gapped and self-hosted

<Engine local>

Nothing is fetched from a third party at runtime unless you point the viewer there. See
[Self-hosting](self-hosting) for every file and the setting that controls it.

</Engine>

<Engine cloud>

The viewer talks to one server: CloudPDF SaaS, or your own CloudPDF when `engine: { baseUrl }`
names it. See [Self-hosting](self-hosting) for running your own and the files the browser loads.

</Engine>

<Callout type="info">
  The viewer is built on the same packages as [headless](/docs/headless). Everything you set up here
  still applies if you build your own interface later.
</Callout>

---

### Content Engine Annotations Appearances (`docs/content/engine/annotations/appearances.mdx`)

---
title: Drawing annotations yourself
description: Render each annotation as its own image, bake annotations into the page, or export them as a PDF.
---

import { Callout } from '@/components/docs/callout';

# Drawing annotations yourself

A rendered page includes its annotations, which is all most apps need. This page is for when you
draw them yourself: each annotation as its own image over the page, so you can move, select and
edit them without rendering the page again. It also covers baking annotations into the page and
exporting them as a PDF.

What you draw is an annotation's **appearance**: the drawing stored with it.

```ts
import { shownAppearances } from '@embedpdf/engine';

const pageImage = await page.render.image({
  viewport: { kind: 'scale', scale: 2 },
  includeAnnotations: false,
}); // the page on its own

const { annotations } = await page.annotations.list();
const { appearances } = await page.annotations.renderAppearances({
  viewport: { kind: 'scale', scale: 2 },
});

// Each annotation as the page shows it now: its look at rest, in the state it shows.
for (const appearance of shownAppearances(appearances, annotations)) {
  const { url } = await appearance.image.objectUrl();
  const box = pageImage.transform.pageToPixels(appearance.rect); // { x, y, width, height } in the page image
  placeImage(url, box);
}
```

<Callout type="info">
  The viewer's annotation plugin does all of this for you. This page is for drawing annotations
  against the engine directly.
</Callout>

## Render each annotation as an image

`renderAppearances()` draws every annotation of the page that has an appearance, each look as
its own image:

| Field   | What it is                                                                                                           |
| :------ | :------------------------------------------------------------------------------------------------------------------- |
| `ref`   | Which annotation it is.                                                                                              |
| `rect`  | Where the image goes, in page space.                                                                                 |
| `mode`  | Which look it is: `'normal'` at rest, `'rollover'` under the pointer, `'down'` while pressed.                        |
| `state` | Which state it draws, for an annotation whose look has states (a check box's `'Off'` and its on state); else `null`. |
| `image` | The image; `image.objectUrl()` gives a URL for an `<img>`, `image.blob()` its bytes.                                 |

| Option     | What it is                                                                                                                                                         | Default    |
| :--------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------- |
| `viewport` | The size, the same option as for [a page image](/docs/engine/documents/rendering#choose-the-size). Pass the page image's `viewport`, and the annotations match it. | scale `1`  |
| `rotation` | The page's rotation as you show it: `0`, `90`, `180` or `270`, clockwise.                                                                                          | `0`        |
| `modes`    | Which looks to render, of those each annotation has. `['normal']` gives only the look at rest. An empty list is refused.                                           | every look |
| `format`   | `'png'` or `'webp'`.                                                                                                                                               | see below  |

<Engine local>

Images are PNG unless you ask for WebP.

</Engine>

<Engine cloud>

Images are WebP unless you ask for PNG. The server sends the whole page's images in one response,
and it's cached until the page's annotations change. Switching a check box changes no image.

</Engine>

### Looks and states

One call brings every look an annotation has, so you can show it at rest, under the pointer and
pressed without asking again:

- **Modes.** An annotation can have a look for when the pointer is over it (`'rollover'`) and for
  when it's pressed (`'down'`), besides the look at rest (`'normal'`). Form fields often do. You
  get one image for each look the annotation has, and none for a look it doesn't have.
- **States.** A check box or radio button has a look for each state: `'Off'` and its on state
  (`'Yes'`, or its export value). You get every state, whatever the check box shows now, each
  image labelled with its `state`. So when someone ticks the box, you already have the picture.
- **Which state shows** is the annotation's `appearanceState` (its `/AS`), or for a form field,
  its value.

`shownAppearances(appearances, annotations)` picks, for each annotation, its look at rest in the
state it shows: the page as it is now. To show a look under the pointer, pick it yourself:

```ts
import { annotationKey } from '@embedpdf/engine';

const hovered = appearances.find(
  (appearance) =>
    annotationKey(appearance.ref) === annotationKey(checkbox.ref) &&
    appearance.mode === 'rollover' &&
    (appearance.state === null || appearance.state === checkbox.appearanceState),
);
```

### Turned annotations

Some annotations can be turned; see [Turned shapes](/docs/engine/annotations/shapes#turned-shapes).
A turned rectangle, ellipse, text box, stamp or caret whose drawing stays inside its turned box
has its image drawn unturned: the image's `rect` is the annotation's own `box`, and you turn the
image yourself, about its centre. `appearanceTurnOf(annotation)` gives the turn to apply, in
degrees clockwise, the same direction as CSS `rotate()`, or `null`:

```ts
import { appearanceTurnOf } from '@embedpdf/engine';

const rotation = appearanceTurnOf(annotation) ?? 0;

img.style.transformOrigin = 'center';
img.style.transform = `rotate(${rotation}deg)`;
```

Every other annotation comes as it is shown, placed by `rect`, with nothing to turn: lines,
polygons and ink are drawn with their points turned, a callout turns only its text box, and a
drawing that reaches past its box (a cloudy border's bumps) is drawn turned.

That split is what keeps editing cheap:

- turning an annotation never needs a new image, only a new transform;
- moving one never needs a new image, only a new position;
- resizing can stretch the image while the user drags, then render once when they let go.

Render again when the annotation's look changed, or for a sharper image at a new zoom. An
update's result tells you: `appearance.changed` is `true` when the annotation looks different.

<Engine local>

### Raw pixels

`renderAppearancesRaw()` takes the same options and returns each annotation's pixels instead of an
image, with nothing encoded. Use it to draw on a canvas or in WebGL.

```ts
const { appearances } = await page.annotations.renderAppearancesRaw({
  viewport: { kind: 'scale', scale: 2 },
});

const { width, height, stride, data } = appearances[0].raster;
```

`data` holds the pixels as RGBA, four bytes a pixel and `stride` bytes a row, without
premultiplied alpha.

</Engine>

## Bake annotations into the page

`flatten()` paints annotations into the page's own content and removes them as annotations.
What's baked in can no longer be changed or deleted as an annotation.

```ts
const result = await page.annotations.flatten([refA, refB], { usage: 'display' });

result.results; // [{ ref: refA, status: 'applied' }, { ref: refB, status: 'unchanged' }]
```

| `status`      | What happened                                                                 |
| :------------ | :---------------------------------------------------------------------------- |
| `'applied'`   | Painted in and removed as an annotation.                                      |
| `'unchanged'` | Left as it was: hidden for that use, a popup, or with no appearance to paint. |

So you can say "2 of 3 flattened".

| `usage`     | What's painted                                        |
| :---------- | :---------------------------------------------------- |
| `'display'` | The annotations shown on screen. The default.         |
| `'print'`   | The annotations that print: those with `print: true`. |

Every ref must be on this page: a ref on another page, or an empty list, refuses the whole call
with `InvalidArg` and nothing changes. When something was painted, an `annotations.flattened`
[event](/docs/engine/concepts/events) fires. Flattening can't be undone, and it ends undo for every
change made before it; see [Changes & undo](/docs/engine/documents/changes#what-cant-be-undone).

To bake every annotation of some pages in one call, flatten the pages with `doc.pages.flatten()`;
see [Flatten a page's annotations](/docs/engine/documents/pages#flatten-a-pages-annotations).

## Export annotations as a PDF

`exportAppearance()` makes a new one-page PDF of some annotations, exactly as the page shows them:
vector, in their places, on a page the size of the box around them. The document doesn't change.

```ts
const pdf = await page.annotations.exportAppearance([refA, refB]);
```

It's all or nothing: a ref that isn't on the page, is hidden, or has no appearance refuses the call
with `InvalidArg`. The PDF is a one-page drawing, so it can become a [stamp](/docs/engine/annotations/stamps):

```ts
await page.annotations.create({ subtype: 'stamp', box: rect }, { resources: { appearance: pdf } });
```

## Permissions

Rendering annotations needs `doc.annotate.read`: seeing an annotation includes seeing how it
looks. Flattening changes the page, so it needs `doc.pages.modify` and `doc.annotate.modify`.
Exporting sends content out, so it needs `doc.download`.

## Good to know

### Appearances, in the PDF

An annotation's appearance is its `/AP`: a small drawing stored with it, with a normal look and
optionally a rollover and a pressed one. The engine draws it for the annotations it
creates, so they look the same in every PDF app. Another app's drawing stays as it is until you
change how that annotation looks.

### Which way `rotation` turns

Every `rotation` in the engine turns clockwise, in degrees, as you see it: a page's rotation, the
`rotation` option on this page, an annotation's own `rotation` and the `rotation` of a run of
turned text. So CSS `rotate()` and the engine always agree.

In the PDF file, an annotation's angle is stored the way PDF measures angles (counterclockwise,
because `y` goes up). The engine converts when it reads and writes, as it does with coordinates,
so you never see the stored number.

### Annotations without a number

Annotations with only a position ref are rendered too. See
[Refs & identity](/docs/engine/concepts/identity).

### What prints

New annotations print unless you create them with `print: false`, so `usage: 'print'` includes
them. Annotations from other apps may have `print` off; set `print: true` on the ones that should
print, or flatten with `'display'`.

---

### Content Engine Annotations File Attachments (`docs/content/engine/annotations/file-attachments.mdx`)

---
title: File attachments
description: Pin a file to a spot on a page, read and download it, and replace it later.
---

# File attachments

A file attachment pins a file to a spot on a page: a spreadsheet next to a table, a drawing next
to a plan. PDF apps show an icon there and open the file from it.

```ts
await page.annotations.create(
  {
    subtype: 'file-attachment',
    rect: { x: 40, y: 732, width: 20, height: 20 },
    file: { name: 'figures.csv', mimeType: 'text/csv' },
  },
  { resources: { file: bytes } },
);
```

The first argument describes the file; its bytes go in the options, as `resources.file`. A `Uint8Array`,
a `Blob` or a `File` works. A `File` the user picked brings its name and type, so it can go in as
it is:

```ts
const picked = input.files![0];

await page.annotations.create(
  { subtype: 'file-attachment', rect: { x: 40, y: 732, width: 20, height: 20 } },
  { resources: { file: picked } },
);
```

| Field              | What it is                                                                       | Default                                  |
| :----------------- | :------------------------------------------------------------------------------- | :--------------------------------------- |
| `rect`             | The icon's box, which the icon fills. Required.                                  |                                          |
| `file.name`        | The file's name, which PDF apps show and save it under. Required for bare bytes. | the `File`'s name                        |
| `file.mimeType`    | The file's type, such as `'text/csv'`.                                           | the `File`'s or `Blob`'s type, or `null` |
| `file.description` | A short description of the file.                                                 | `null`                                   |
| `icon`             | `'paperclip'`, `'push-pin'`, `'graph'` or `'tag'`                                | `'paperclip'`                            |
| `color`            | The icon's color, `'#rrggbb'`                                                    | yellow                                   |
| `opacity`          | From `0` (invisible) to `1` (solid)                                              | `1`                                      |

The icon fills `rect`, as in every PDF app; 20 × 20 points is the usual size. Move or resize the
icon by its `rect`. Any bytes are a valid file. The engine never guesses a type: without one, `mimeType` reads `null`.

## Read and download the file

A read describes the file; `downloadResource()` returns its bytes, exactly as they were attached:

```ts
const { annotations } = await page.annotations.list();
const attachment = annotations.find((annotation) => annotation.subtype === 'file-attachment');

if (attachment?.file) {
  console.log(attachment.file.name, attachment.file.size);
  const bytes = await page.annotations.downloadResource(attachment.ref, 'file');
}
```

A read's `file` has the name, type and description you wrote. It also has these fields, which the
engine fills in:

| Field        | What it is                                     |
| :----------- | :--------------------------------------------- |
| `size`       | The file's size in bytes.                      |
| `checksum`   | The MD5 of the file's bytes, in lowercase hex. |
| `createdAt`  | When the file was attached, as an ISO date.    |
| `modifiedAt` | When the file was last changed.                |

The engine writes `size`, `checksum` and `createdAt` when it attaches a file. What the PDF doesn't
record reads as `null`: `modifiedAt` for a file attached here, and any of the four for some files
other apps attached. `file` itself is `null` for an attachment another app made without an
embedded file.

To let the user save it, wrap the bytes in a `Blob`:

```ts
const blob = new Blob([bytes], { type: attachment.file.mimeType ?? '' });
```

## Replace the file

`file` in an update replaces the name, type and description together; new bytes replace the
file itself:

```ts
await page.annotations.update(attachment.ref, {
  file: { ...attachment.file, name: 'figures-2026.csv' },
});

await page.annotations.update(attachment.ref, {}, { resources: { file: newBytes } });
```

A field of `file` you leave out is removed, so start from the one you read, as above. `size`,
`checksum` and the dates in it are ignored: the engine writes those itself. An attachment can't
lose its file (`file: null` is refused with `InvalidArg`); delete the annotation instead.

The icon, its color and its opacity change like any field: `{ icon: 'tag' }`.

## Copy an attachment

The read and the bytes together make the same attachment again, on another page or in another
document:

```ts
const bytes = await page.annotations.downloadResource(attachment.ref, 'file');

await otherPage.annotations.create(attachment, { resources: { file: bytes } });
```

## Files on a page and files of the document

A PDF can also carry files that belong to the document as a whole rather than to a spot on a
page. Those are [attachments](/docs/engine/documents/attachments), with their own calls. The two
don't mix: `doc.attachments.list()` doesn't list the files pinned to pages, and the calls on this
page don't see the document's files.

## Limits

- A name can't be empty, and a create without the `file` bytes is refused with `InvalidArg`.
- To [export](/docs/engine/annotations/import-export) an attachment, its file must fit a bundle's
  limit for one resource: 50 MB. A larger one refuses the export with `PayloadTooLarge`.

<Engine cloud>

- The bytes travel to the server with the create or the update, so a file can be at most the
  server's upload limit: 50 MB unless your server sets another. A larger one is refused with
  `PayloadTooLarge`.

</Engine>

## Permissions

Creating, changing and deleting file attachment annotations (not `doc.attachments.modify`) needs
the same permissions as any annotation (see
[Permissions](/docs/engine/concepts/permissions#annotations)). Downloading the file sends document
content out, so it needs `doc.download`.

## Good to know

### Dates

`createdAt` is written when the file is attached, in UTC. An
[import](/docs/engine/annotations/import-export#who-wrote-it) keeps the source file's dates,
whichever attribution it uses: they're facts about the file, not about who wrote the annotation.

### Icons from other apps

In the PDF, `icon` is the annotation's `/Name`. A name other than the four standard ones reads as
`'push-pin'`, which is also what PDF apps draw for it.

### How a copy looks

A copy's icon is the engine's own icon, filling the same `rect`, whatever icon another app drew
for the original.

---

### Content Engine Annotations Free Text (`docs/content/engine/annotations/free-text.mdx`)

---
title: Free text
description: Put text boxes and callouts on a page, in any font, with bold, italic and colored words.
---

import { Callout } from '@/components/docs/callout';

# Free text

A free text annotation writes text straight onto the page: a text box, or a callout with a line
that points at something. The engine lays out the text and draws it, so it looks the same in
every PDF app.

```ts
const { annotation } = await page.annotations.create({
  subtype: 'free-text',
  box: { x: 72, y: 132, width: 200, height: 60 },
  contents: 'Please check this section',
  fontSize: 14,
});
```

## Text boxes

The text wraps inside `box`. Start a new line with `\n` in `contents`; it reads back as `\r`,
the line break PDF uses.

| Field           | What it is                                                       | Default       |
| :-------------- | :--------------------------------------------------------------- | :------------ |
| `box`           | The text box. Required.                                          |               |
| `contents`      | The text, plain.                                                 | empty         |
| `fontFamily`    | The font; see [Fonts](#fonts).                                   | `'helvetica'` |
| `fontSize`      | The text size in points.                                         | `12`          |
| `textAlign`     | `'left'`, `'center'`, `'right'` or `'justify'`.                  | `'left'`      |
| `verticalAlign` | Where the lines sit in the box: `'top'`, `'middle'`, `'bottom'`. | `'top'`       |
| `fontColor`     | The text color.                                                  | black         |
| `color`         | The border color, and a callout's line.                          | black         |
| `interiorColor` | The background, or `null` for none.                              | none          |
| `strokeWidth`   | The border width in points. `0` for no border.                   | `1`           |
| `opacity`       | From `0` to `1`, for the whole box: border, background and text. | `1`           |

A read also has `intent`: `'free-text-callout'` for a box with a [callout line](#callouts),
`'free-text'` for the others. The engine sets it from the `calloutLine`; you don't need to. A
box another app made as a typewriter reads `'free-text-typewriter'` and stays one; you can't
create one.

`borderStyle` and `dashArray` work as for every annotation; see the
[overview](/docs/engine/annotations#style). A cloudy border takes `cloudyIntensity`, as on
[shapes](/docs/engine/annotations/shapes).

## Colors

A free text box has three colors, each with its own job:

- `fontColor` colors the text.
- `color` draws the border and a callout's line.
- `interiorColor` fills the box. Leave it out for a transparent box.

Acrobat keeps them apart the same way, so a box from Acrobat reads the colors it shows there.

```ts
await page.annotations.create({
  subtype: 'free-text',
  box: { x: 72, y: 212, width: 200, height: 60 },
  contents: 'Approved',
  fontFamily: 'helvetica-bold',
  fontSize: 18,
  textAlign: 'center',
  color: '#006400', // green border
  fontColor: '#000000', // black text
  interiorColor: '#e6ffe6', // pale green background
});
```

## Fonts

`fontFamily` takes one of the 14 standard PDF fonts. Every PDF app has them, so nothing is
embedded in the file:

`helvetica`, `helvetica-bold`, `helvetica-oblique`, `helvetica-bold-oblique`, `times-roman`,
`times-bold`, `times-italic`, `times-bold-italic`, `courier`, `courier-bold`, `courier-oblique`,
`courier-bold-oblique`, `symbol`, `zapf-dingbats`.

The standard fonts only cover Latin text.

<Engine local>

For other scripts (Chinese, Arabic, Cyrillic…) or your own typeface, register a font with the
engine and pass its key as `fontFamily`:

```ts
await engine.fonts.register({ key: 'noto-sc', data: fontBytes });

await page.annotations.create({
  subtype: 'free-text',
  box: { x: 72, y: 132, width: 200, height: 60 },
  contents: '请检查这一部分',
  fontFamily: 'noto-sc',
  fontSize: 14,
});
```

When you save, the engine embeds only the characters the annotation uses. Reading the annotation
back gives `fontFamily: 'noto-sc'` again. A key that isn't registered fails with `InvalidArg`. See
[Fonts](/docs/engine/documents/fonts) for registering fonts and filling in missing characters.

</Engine>

<Engine cloud>

For other scripts (Chinese, Arabic, Cyrillic…) or your own typeface, pass the key of a font your
server is configured with as `fontFamily`. The server also uses those fonts to draw characters the
chosen font doesn't have, so text in any script your server has a font for shows up. The fonts are
the server's to choose, for every user and every document; see
[Fallback fonts](/docs/server/configuration/fonts).

</Engine>

## Callouts

A callout is a text box with a line that points at something on the page. Give the box a
`calloutLine`:

```ts
await page.annotations.create({
  subtype: 'free-text',
  box: { x: 300, y: 132, width: 180, height: 60 }, // the text box
  calloutLine: [
    { x: 200, y: 272 }, // the point it points at
    { x: 250, y: 162 }, // a bend (optional)
    { x: 300, y: 162 }, // where the line meets the box
  ],
  lineEnding: 'open-arrow',
  contents: 'This total is wrong',
});
```

| Field         | What it is                                                                                                                        | Default |
| :------------ | :-------------------------------------------------------------------------------------------------------------------------------- | :------ |
| `calloutLine` | Two points (straight) or three (with a bend). The first is what it points at, the last touches the box.                           | none    |
| `lineEnding`  | What's drawn at the point it points at, such as `'open-arrow'`; see [Line endings](/docs/engine/annotations/shapes#line-endings). | none    |

`box` stays the text box; the engine makes room for the line and its arrow in `rect`, where the
callout sits on the page. A callout line goes only with `intent: 'free-text-callout'`, which a
create sets for you; elsewhere it fails with `InvalidArg`. To add a line to a plain box, send
both, and to turn a callout back into a plain box, send `calloutLine: null` and
`intent: 'free-text'`.

## Rich text

Every free text also has a `richText` document, for text with bold, italic or colored words. It
has a `body`, the style of the whole box, and `paragraphs` of `runs`. A run is a piece of text
with a `style` that holds only what it changes:

```ts
await page.annotations.create({
  subtype: 'free-text',
  box: { x: 72, y: 132, width: 300, height: 60 },
  fontSize: 14,
  richText: {
    paragraphs: [
      {
        runs: [
          { text: 'Total is ' },
          { text: 'wrong', style: { weight: 700, color: '#CC0000' } },
          { text: ', see page 4.' },
        ],
      },
    ],
  },
});
```

The `body` is the same style as `fontFamily`, `fontSize`, `fontColor` and `textAlign`, so you
rarely write one. When you send both, the field wins: `fontColor: '#CC0000'` with a `body` of
`{ color: '#0000CC' }` makes red text. `contents` always reads back as the plain text:
`'Total is wrong, see page 4.'`, with paragraphs joined by `\r`.

A style can hold:

| Property          | What it is                                                                                 |
| :---------------- | :----------------------------------------------------------------------------------------- |
| `family`          | The font, by family name: `'Helvetica'`, `'Times'`, `'Courier'`, or a font the engine has. |
| `weight`          | From `100` to `900`: `400` is regular, `700` bold.                                         |
| `italic`          | `true` for italic.                                                                         |
| `size`            | The size in points.                                                                        |
| `color`           | A hex color, `'#RRGGBB'`.                                                                  |
| `decoration`      | `['underline']`, `['line-through']`, or both.                                              |
| `script`          | `'normal'`, `'sub'` or `'super'`.                                                          |
| `letterSpacing`   | Extra points after every character.                                                        |
| `horizontalScale` | Character width, `1` for normal.                                                           |

The `body`, and a paragraph that differs from it, also take `align` (`'left'`, `'center'`,
`'right'` or `'justify'`) and `dir` (`'ltr'` or `'rtl'`).

Runs change only what they name, so a change to the body moves every run that didn't set its
own value: make the body red and a blue word stays blue.

<Callout type="info">
  Editing rich text in the browser? The [rich text editor](/docs/headless/concepts/rich-text) in the
  annotation plugin does the typing, the selection and the formatting for you.
</Callout>

## Change the text

What an update does depends on what you send:

| You send                                           | What happens                                                                                                    |
| :------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------- |
| `contents`                                         | The text is replaced with plain paragraphs in the body style. Bold words and other run styles go.               |
| `richText` without `body`                          | The text is replaced; the body style stays.                                                                     |
| `richText` with `body`                             | The text is replaced, and the body style changes where the `body` says.                                         |
| `fontFamily`, `fontSize`, `fontColor`, `textAlign` | The body style changes; runs keep their own styles.                                                             |
| `contents` and `richText`                          | They must be the same text (`contents` is the plain text of `richText`), or the update fails with `InvalidArg`. |

```ts
// Make one word italic, keep everything else:
await page.annotations.update(annotation.ref, {
  richText: {
    paragraphs: [
      {
        runs: [
          { text: 'Please check ' },
          { text: 'this', style: { italic: true } },
          { text: ' section' },
        ],
      },
    ],
  },
});
```

A `body` changes only the properties it names, like any other update: a body of `{ size: 18 }`
makes the text bigger and keeps its font, color and alignment.

## Permissions

Reading free text needs `doc.annotate.read`. Creating, changing and deleting it needs the same
permissions as any annotation; see [Permissions](/docs/engine/annotations#permissions).

## Good to know

### Where the text sits

The text starts twice the border width inside the box, on every side, the way Acrobat lays it
out: a 1-point border leaves 2 points around the text. Text that doesn't fit in that area is cut
off.

PDF has no entry for where the lines sit in the box, so `verticalAlign` is the engine's own:
other PDF apps show the box as the engine drew it, and put the text at the top if they draw it
again themselves.

### Justified text

PDF's own alignment entry has no _justify_, so `'justify'` is saved in the rich text only. A PDF
app that reads just the plain text shows a justified box left-aligned.

### Fonts inside rich text

A run names its font by family, the way the PDF does: `'Helvetica'`, `'Times'`, `'Courier'`, the
family name of a font the engine has, or a font the document already embeds. A family the engine
can't find is drawn in Helvetica; the run keeps the name you gave. `fontFamily` and `fontSize`
always read back the body's font and size.

### Turned boxes

A free text box turns like a rectangle: set `box` and `rotation` (see
[Shapes](/docs/engine/annotations/shapes#turned-shapes)). On a callout only the text box turns;
the line stays where it is.

Acrobat turns a text box only a quarter at a time, when you add one on a turned page. Such a box
reads its `rotation` (90, 180 or 270), and a box you turn by a quarter opens in Acrobat as its
own. A box turned by another angle shows turned in Acrobat too, but Acrobat straightens it when
you edit its text there.

### How it's stored

For readers who know PDF: `color` is the color in `/DA`, `fontColor` is the color of the rich
text's body, `interiorColor` is `/C`, and `richText` is written as `/RC` (XHTML) and `/DS`, the
form Acrobat reads and writes. `verticalAlign` is kept in the annotation's `/EMBD_Metadata`. `contents` is
`/Contents`, `calloutLine` is `/CL` and `intent` is `/IT`. For a callout, the PDF's own `/Rect`
takes in the line, and `/RD` says where the text box is inside it, as Acrobat writes it; the
engine writes both from `box`.

---

### Content Engine Annotations Import Export (`docs/content/engine/annotations/import-export.mdx`)

---
title: Import & export
description: Take annotations out of a document as one bundle, store it, and put it into the same document or another in one call.
---

# Import & export

Export takes annotations out of a document, with the bytes they carry, as one **bundle**. Import
puts a bundle's annotations into a document. Each is one call:

```ts
const bundle = await source.annotations.export();

const { annotations, dropped } = await target.annotations.import(bundle, { attribution: 'stamp' });
```

Use it to copy annotations to a new version of a document, to keep them in your own database
apart from the PDF, or to move a review from one file to another.

## Export

```ts
const everything = await doc.annotations.export();
const thread = await doc.annotations.export({ refs: [comment.ref] }); // a comment and its replies
const onPage = await doc.annotations.export({ pages: [pageRef] }); // everything on one page
```

| Option    | What it is                                                               | Default     |
| :-------- | :----------------------------------------------------------------------- | :---------- |
| `refs`    | The annotations to export.                                               | all         |
| `pages`   | The pages whose annotations to export.                                   | all         |
| `include` | `'threads'`: also every reply below what you chose. `'references'`: not. | `'threads'` |

A selection brings what it needs, so nothing arrives broken:

- a reply brings the comment it answers, up to the first comment of the thread;
- an annotation brings its popup, and a popup brings its annotation;
- with `include: 'threads'`, a comment brings every reply below it.

A page or an annotation the document doesn't have refuses the export with `NotFound`.

## Store it as a file

`AnnotationTransfer` writes a bundle as one JSON text, with its resources in base64, and reads it
back:

```ts
import { AnnotationTransfer } from '@embedpdf/engine';

const text = AnnotationTransfer.stringify(bundle);
const again = await AnnotationTransfer.parse(text);
```

`parse()` checks the [limits](#limits) first, so a file that's too large is refused before it's
decoded. Then it checks:

- the format and version;
- that every resource matches its id;
- that every resource an item names is there;
- that every resource is named by an item.

Anything else is refused with `InvalidArg`. For what a bundle holds, see
[What's in a bundle](#whats-in-a-bundle).

## Import

```ts
const result = await doc.annotations.import(bundle, { attribution: 'stamp' });

result.annotations; // the new annotations, in bundle order
result.refMap; // [{ from, to }]: each annotation's ref in the bundle, and its ref here
result.dropped; // what was left out, and why
```

| Option        | What it is                                                             | Default     |
| :------------ | :--------------------------------------------------------------------- | :---------- |
| `pages`       | Which page each bundle page goes to.                                   | `'same'`    |
| `attribution` | `'restore'` keeps who wrote each annotation; `'stamp'` makes it yours. | `'restore'` |
| `opId`        | A name for this import: its events carry it, and it names its undo.    | a new id    |

Everything is checked before anything is written. The import is all or nothing: a failure while
writing leaves the document exactly as it was, with no new annotations and no events.

### Pages

By default each annotation goes to the page with the same ref, which fits a bundle from the same
document or a copy of it. For another document, place the pages by their position, or name the
pairs:

```ts
await doc.annotations.import(bundle, { attribution: 'stamp', pages: 'by-position' });

await doc.annotations.import(bundle, {
  attribution: 'stamp',
  pages: [{ from: bundlePage, to: pageRef }],
});
```

A link's destination is placed the same way. Every page an annotation is on, or a link points to,
needs a match; otherwise the import fails with `InvalidArg` and `err.details.pages` lists them. A
`to` the document doesn't have is `NotFound`.

Positions in a bundle are measured from the page's top-left corner, like every position in the
engine, so an annotation lands as far from the top-left of the page it goes to as it was on its
own page, whatever the two pages' crop boxes.

### Who wrote it

With `attribution: 'restore'`, the default, each annotation keeps its author, its creation and
change dates, its owner (`userId`, `createdBy`, `modifiedBy`) and its `groupId`. The user who
imports is recorded in `importedBy`. That writes annotations in other people's names, so it needs
`doc.annotate.modify` and `doc.annotate.import`: give them to trusted jobs, such as a sync from
your own database, not to end users.

With `attribution: 'stamp'`, each annotation is made as `create` makes it: the user who imports
is its author and owner, and the dates are now. It needs what creating each annotation needs and
nothing more, so a user who may create their own annotations may paste them. It's the one to use
for a user pasting annotations.

In both modes an attached file keeps its own dates: they're facts about the file, not about who
wrote the annotation.

### What's left out

What an import can't carry it leaves out, lists in `dropped`, and imports the rest. Each entry is
`{ ref, reason }`, the `ref` as the bundle names it. An entry with a `field` means only that field
was left out: the annotation itself was imported without it.

| `reason`             | What was left out                                                                                                                                                       |
| :------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unsupported-kind`   | An annotation of a kind the engine doesn't model.                                                                                                                       |
| `widget`             | A widget, in a field or not: widgets are the form's, and travel with it through [forms](/docs/engine/forms).                                                            |
| `name-conflict`      | An annotation whose `nm` ([its unique name](/docs/engine/concepts/identity)) is taken on its page, or by an earlier item.                                               |
| `parent-missing`     | A reply or popup whose parent isn't in the bundle, on the same page.                                                                                                    |
| `parent-dropped`     | A reply or popup whose parent was left out.                                                                                                                             |
| `geospatial`         | Field `measure`: a map's scale, which the engine can't write.                                                                                                           |
| `unknown-measure`    | Field `measure`: a scale of a kind the engine doesn't know.                                                                                                             |
| `unsupported-action` | Field `target` or `actions`: what a link does, or actions, that the engine can't write.                                                                                 |
| `unsupported-value`  | A value another app wrote that a write can't make, such as a shape's beveled border: that field, or the whole annotation when it's its shape (a polyline of one point). |
| `popup-taken`        | A second popup for the same annotation: an annotation has one.                                                                                                          |

Because of `name-conflict`, importing the same bundle twice doesn't duplicate the annotations that
have a name. A reply or popup whose parent is left out goes too, so a thread never arrives in
pieces.

### Events

An import fires one `annotations.created` event per annotation, all with the same `origin.tx`, so
you can update your interface once for the whole batch. See
[Which write an event came from](/docs/engine/concepts/events#which-write-an-event-came-from).

### Undo an import

An import is one change. `doc.apply({ undoOf })` with its `opId` takes it back, and the undo's
undo brings back the same annotations, with the same refs and the attribution the import wrote:

```ts
const imported = await doc.annotations.import(bundle);
const undo = await doc.apply({ undoOf: imported.meta.opId });
await doc.apply({ undoOf: undo.meta.opId }); // back again
```

The undo removes each imported thread whole: a note with its popup and replies. An imported
annotation that was changed since, or got a reply, stays, and its item is `skipped`; the rest of
the import goes. As for every change, only the user who imported may undo it, and a final write
ends undo for the imports before it. See [Changes & undo](/docs/engine/documents/changes).

## Limits

One set of limits applies to making and reading bundles, so whatever an export makes, an import
takes:

| Limit                    | Default           |
| :----------------------- | :---------------- |
| The whole bundle         | 50 MB             |
| Items and pages, as JSON | 16 MB             |
| Annotations              | 10,000            |
| Pages                    | 2,000             |
| Resources                | 2,000             |
| One resource             | 50 MB             |
| One image, decoded       | 40 million pixels |

Past a limit, the call fails with `PayloadTooLarge`, and `err.details.limit` names the limit.
Export checks while it builds the bundle; export fewer pages at a time to stay under.

<Engine cloud>

The server holds the limits, with these defaults unless you set others. The client checks a
bundle against the server's limits before it sends it, so a bundle past one fails before its bytes
move.

An import is one request, however many annotations and resources it carries. Its `opId`, made for
you when you don't pass one, makes a retry safe: a second request with the same `opId` returns
what the first one did and writes nothing.

An export is cached until the document's annotations or pages change, so exporting again is cheap.

</Engine>

## Permissions

Export reads the annotations and the bytes they carry, so it needs `doc.annotate.read` and
`doc.download`.

An import with `attribution: 'stamp'` needs what creating the annotations needs: a user who may
create their own annotations (`annotations:create:self`) may paste a bundle. An import with
`attribution: 'restore'` needs `doc.annotate.modify` and `doc.annotate.import`. See
[Permissions](/docs/engine/concepts/permissions).

Undoing an import takes what the import took. For a `'restore'` import that's
`doc.annotate.modify` and `doc.annotate.import`, not the rules for deleting other people's
annotations: the importer can take back annotations that carry someone else's `userId`.

## Good to know

### What's in a bundle

```ts
bundle.items; // [{ data, resources: { appearance: 'sha256-…' } }, …]
bundle.resources; // { 'sha256-…': Uint8Array }
bundle.pages; // [{ page, position, size }, …]
```

- Each item's `data` is the annotation exactly as a read returns it, in page order and then in
  the page's own order.
- The bytes an annotation carries (a stamp's drawing, an attached file) are `resources`, each
  stored once under the SHA-256 of its bytes. A hundred stamps of one logo carry the logo once.
- `pages` lists every page the items are on or point at, with its position in the document
  (from `0`) and its `size`, so the annotations can be placed on the right pages of another
  document.

A bundle is plain data. You can store each item as a row and each resource once under its id,
and build a bundle again from your database without the engine.

### A copy is drawn from its data

An import redraws each annotation from its fields, as FDF and XFDF imports do. Only stamps keep
their original drawing. So:

- a highlight, a drawing or a text box that another app drew looks as the engine draws the same
  data;
- the engine works out `rect` from what it draws (lines, polygons and ink from their points,
  highlights from their quads, the other shapes from their `box`), so it can differ slightly
  from the original;
- a note's and a file's icon is the engine's own icon, filling the same `rect`;
- a measurement's label is worked out again from its scale.

### The round trip

Export a document, import the bundle into a copy of its pages, and export that: when nothing was
dropped, you get the same bundle, with the same data and every resource the same bytes under the
same id. Only the refs change, and the attribution: `importedBy` with `'restore'`, author and
dates with `'stamp'`. The same document also exports to the same bundle on the local and the cloud
engine.

### What an import doesn't do

- Link a reply to a comment that's already in the target document: replies and popups link only
  to items of the same bundle.
- Move or scale annotations, to paste them at an offset or onto a page of another size.
- Carry what the engine doesn't model: those annotations and fields are in `dropped`.

### Bundles you build

Resource ids are `sha256-` and the SHA-256 of the bytes in lowercase hex. A stamp's appearance in
a bundle you build may be PNG or JPEG bytes, as `create` takes; an export always gives a one-page
PDF.

---

### Content Engine Annotations Links (`docs/content/engine/annotations/links.mdx`)

---
title: Links
description: Make an area of a page go to another page or open a website, and read what existing links do.
---

# Links

A link is an area of a page that does something when it's clicked: go to another page of the
document, or open a website.

```ts
await page.annotations.create({
  subtype: 'link',
  rect: { x: 72, y: 77, width: 128, height: 15 },
  target: { kind: 'uri', uri: 'https://example.com' },
});
```

A link is an area (`rect`) and what a click does (`target`). It's invisible: it has no color or
border, and PDF apps draw nothing where it is. It takes the fields every annotation has, such as
`contents` and the flags. Leave `target` out for a link that does nothing yet (`null`).

## Link to a page

A `goto` target names a page and how to show it:

```ts
const { pages } = await doc.pages.list();

await page.annotations.create({
  subtype: 'link',
  rect,
  target: { kind: 'goto', destination: { kind: 'fit', page: pages[4].ref } },
});
```

| `kind`    | What the reader sees                                         | Fields                      |
| :-------- | :----------------------------------------------------------- | :-------------------------- |
| `'fit'`   | The whole page.                                              |                             |
| `'xyz'`   | The point `x`, `y` at the top left of the window, at `zoom`. | `x`, `y`, `zoom`            |
| `'fitH'`  | The page's full width, with `y` at the top of the window.    | `y`                         |
| `'fitV'`  | The page's full height, with `x` at the left of the window.  | `x`                         |
| `'fitR'`  | The box `x`, `y`, `width`, `height`, as large as it fits.    | `x`, `y`, `width`, `height` |
| `'fitB'`  | Like `'fit'`, for the box around the page's content.         |                             |
| `'fitBH'` | Like `'fitH'`, for the box around the page's content.        | `y`                         |
| `'fitBV'` | Like `'fitV'`, for the box around the page's content.        | `x`                         |

Positions are in points on the target page, from its top-left corner, with y going down. `zoom`
is a factor: `2` shows the page at 200%. A position or zoom left out (or `null`) keeps what the
reader has, so this link scrolls to a spot without changing the zoom:

```ts
await page.annotations.update(link.ref, {
  target: {
    kind: 'goto',
    destination: { kind: 'xyz', page: pages[4].ref, x: null, y: 292, zoom: null },
  },
});
```

A link to a page the document doesn't have is refused with `NotFound`.

## Link to a website

A `uri` target opens its address, as you give it:

```ts
await page.annotations.update(link.ref, {
  target: { kind: 'uri', uri: 'https://example.com/pricing' },
});
```

## Read links

```ts
const { annotations } = await page.annotations.list();

for (const annotation of annotations) {
  if (annotation.subtype !== 'link' || !annotation.target) continue;
  const { target } = annotation;
  if (target.kind === 'goto') showPage(target.destination.page);
  if (target.kind === 'uri') openInNewTab(target.uri);
}
```

`target` says what a click does:

| `kind`          | What a click does                                  | You can create it |
| :-------------- | :------------------------------------------------- | :---------------- |
| `'goto'`        | Goes to a page of this document (`destination`).   | Yes               |
| `'uri'`         | Opens a web address (`uri`).                       | Yes               |
| `'named'`       | Runs a viewer command such as `NextPage` (`name`). | The four below    |
| `'goto-remote'` | Opens another PDF file (`file`).                   | No                |
| `'launch'`      | Opens a file or an app (`path`).                   | No                |
| `'javascript'`  | Runs a script.                                     | No                |
| `'unsupported'` | Something the engine doesn't model.                | No                |

`target: null` is a link that does nothing. A PDF can name a destination instead of spelling it
out; the engine looks the name up, so you always get an explicit `destination` with its page.

A `named` link you create runs one of the page commands every PDF app has: `NextPage`,
`PrevPage`, `FirstPage` or `LastPage`. Other names, which only some apps know, are read and kept:

```ts
await page.annotations.create({
  subtype: 'link',
  rect,
  target: { kind: 'named', name: 'NextPage' },
});
```

A new link has no border: PDF apps would otherwise draw a black line around it.

## Change or remove what a link does

```ts
await page.annotations.update(link.ref, {
  target: { kind: 'goto', destination: { kind: 'fit', page: pages[0].ref } },
});

await page.annotations.update(link.ref, { target: null }); // does nothing when clicked
```

A new target replaces the old one, and `null` removes it.

## What the engine doesn't run

Links that run a script, open another file or run another app's own command are read and kept
in the PDF as they are. The engine never runs or follows them, and you can't create them: a `target` of one
of these kinds in a create is refused with `InvalidArg`. Changing another field of such a link
(its `rect`, its `contents`) keeps what it does, and so does sending back a link you read: its
`target` stays as it was.

## Permissions

Links are annotations: they need the same permissions as any annotation. See
[Permissions](/docs/engine/concepts/permissions#annotations).

## Good to know

### The full action

`target` is a simple view. The full action is in `annotation.actions` (read only). A click runs
`actions.activate`; later steps are in `next`. An action the engine can't read safely reads as
`unsupported`. Writing `target` replaces the whole action.

### Destinations and actions in the PDF

A PDF stores a link's target as a destination (`/Dest`) or as an action (`/A`). Both read as a
`goto` target; the engine writes an action. `target: null` removes both.

### Coordinates

A destination's positions are measured on the page it goes to, like that page's annotations:
from its top-left corner, with y going down. `pageDestinationOf()` and `pdfDestinationOf()`
convert to and from the PDF file's own numbers, with the crop box of the page it goes to; see
[Pages](/docs/engine/documents/pages).

### Copying links

An [import](/docs/engine/annotations/import-export) maps a link's destination page the way it
maps the link's own page. A link whose target the engine can't create is imported without it, and
the import reports it in `dropped` (`unsupported-action`, field `target`).

---

### Content Engine Annotations Measurements (`docs/content/engine/annotations/measurements.mdx`)

---
title: Measurements
description: Measure distances, perimeters and areas at a real-world scale, with labels the engine keeps up to date.
---

# Measurements

A measurement is a line, a path or a shape that shows a real-world size, such as "2.00 m" or
"12.45 m²". You give the engine the scale of the drawing; it works out the value and writes the
label.

```ts
import { measureFromKnownLength } from '@embedpdf/engine';

// 144 points on this drawing are 2 metres in reality.
const scale = measureFromKnownLength(144, { value: 2, unit: 'm' });

const { annotation } = await page.annotations.create({
  subtype: 'line',
  intent: 'line-dimension',
  measure: scale,
  linePoints: { start: { x: 72, y: 90 }, end: { x: 216, y: 90 } },
  captionEnabled: true,
});

annotation.contents; // "2.00 m"
```

## Distance, perimeter and area

A measurement is a line, polyline or polygon annotation with a measuring `intent` and a `measure`
(the scale):

| Measurement | `subtype`    | `intent`               | Its points   |
| :---------- | :----------- | :--------------------- | :----------- |
| Distance    | `'line'`     | `'line-dimension'`     | `linePoints` |
| Perimeter   | `'polyline'` | `'polyline-dimension'` | `vertices`   |
| Area        | `'polygon'`  | `'polygon-dimension'`  | `vertices`   |

Everything else works as it does for [shapes](/docs/engine/annotations/shapes): style, flags,
moving, deleting.

```ts
await page.annotations.create({
  subtype: 'polygon',
  intent: 'polygon-dimension',
  measure: scale,
  vertices: [
    { x: 100, y: 100 },
    { x: 200, y: 100 },
    { x: 200, y: 200 },
    { x: 100, y: 200 },
  ],
  captionEnabled: true,
});
```

A perimeter adds up the segments of the open path; it doesn't add a segment back to the start.
An area needs one simple outline: edges that cross or overlap give no value, rather than a wrong
one.

## The scale

Two helpers make a scale, from a length you know or from the drawing's ratio:

```ts
import { measureFromKnownLength, measureFromRatio } from '@embedpdf/engine';

measureFromKnownLength(144, { value: 2, unit: 'm' }); // 144 points on the page are 2 m
measureFromRatio(1, 100, 'm'); // the drawing is at 1:100, shown in metres
```

A known length is in points, for example the length of a line the user drew over a dimension
printed on the plan. A ratio compares a length on paper with the same length in reality.

Units are `'pt'`, `'mm'`, `'cm'`, `'m'`, `'km'`, `'in'`, `'ft'`, `'yd'` and `'mi'`. The last
argument sets how values are shown:

```ts
measureFromKnownLength(144, { value: 2, unit: 'm' }, { precision: 10, areaUnit: 'ha' });
measureFromKnownLength(72, { value: 10, unit: 'ft' }, { imperialFeetInches: true });
```

| Option                | What it does                                                               | Default                |
| :-------------------- | :------------------------------------------------------------------------- | :--------------------- |
| `precision`           | Decimals, as `1`, `10`, `100`, …: `100` shows two.                         | `100`                  |
| `areaUnit`            | The unit for areas: `'m2'`, `'ft2'` and so on, `'ha'` or `'acre'`.         | the square of the unit |
| `imperialFeetInches`  | Show feet and inches, such as `1 ft 6 in`.                                 | `false`                |
| `fractionalPrecision` | With feet and inches: inches in steps of `1/16`, `1/8`, …                  | `16`                   |
| `userUnit`            | For a ratio: the page's `userUnit`, when its points are larger than usual. | `1`                    |

```ts
const { pages } = await doc.pages.list();

measureFromRatio(1, 100, 'm', { userUnit: pages[0].userUnit });
```

Both helpers throw an `EngineError` with code `InvalidArg` for a length, value or ratio that isn't
a positive number.

## Labels

The engine writes the label into `contents`: when you create a measurement, and whenever you
change its points, its `measure` or its `intent`. You can't set another label. A `contents` you
send is replaced by the value, so the label is always what the scale says:

```ts
await page.annotations.update(annotation.ref, {
  linePoints: { start: { x: 72, y: 90 }, end: { x: 288, y: 90 } },
});
// contents is now "3.00 m"
```

A change to style or flags leaves the label as it is, so a label another app wrote stays as it
wrote it. So does a measurement without a scale the engine can use.

Labels show every decimal the precision asks for, as Acrobat does: `3.00 m`, `1.50 m`.

### Show a value before you create it

`measurementReadout()` works out what the engine would write, for example while the user is
still drawing:

```ts
import { measurementReadout } from '@embedpdf/engine';

const readout = measurementReadout(draft);

if ('label' in readout)
  showLabel(readout.label); // "12.45 m²"
else showHint(readout.unavailable); // "invalid-geometry"
```

| Field       | What it is                                     |
| :---------- | :--------------------------------------------- |
| `kind`      | `'distance'`, `'perimeter'` or `'area'`.       |
| `label`     | The label, as the engine writes it.            |
| `value`     | The number, in the label's (first) unit.       |
| `perimeter` | For an area, the length around it, as a label. |

When there's no value, `unavailable` says why; see
[Why a readout has no value](#why-a-readout-has-no-value).

`formatMeasurement(value, formats)` formats any number the way a scale does, such as
`formatMeasurement(1.5, scale.distance)` for `"1.50 m"`.

## Captions

`captionEnabled: true` draws the label on the measurement. Whether a label shows and where it
sits are separate fields, so hiding a label keeps its position.

| Field             | Kind            | What it is                                                                     | Default    |
| :---------------- | :-------------- | :----------------------------------------------------------------------------- | :--------- |
| `captionEnabled`  | all             | Draw the label. `null` when it was never set.                                  | `null`     |
| `captionPosition` | distance        | `'inline'` (in the line) or `'top'` (above it).                                | `'inline'` |
| `captionOffset`   | distance        | `{ along, perpendicular }`: points along the line and square to it, or `null`. | `null`     |
| `captionCenter`   | perimeter, area | The label's centre `{ x, y }`, upright like the points, or `null`.             | `null`     |
| `leader`          | distance        | Leader lines at the ends: `{ length, extension?, offset? }` in points.         | none       |

`null` for `captionOffset` or `captionCenter` places the label automatically. `captionEnabled:
false` hides the label and keeps its place. On a perimeter or area, `captionEnabled: null` removes
the caption and its place together.

A label you placed moves with the shape when you move or turn the whole shape, and stays put when
you edit single points. A `captionCenter` you send wins. On a [turned shape](/docs/engine/annotations/shapes#turned-shapes)
it is upright, like the points, and turns with them.

A measurement without a caption reads `captionEnabled: null`, a distance line too. One another app
made keeps its look until you turn a caption on.

## Page calibration

A page can carry its own scale, for PDF apps and measuring tools to use when someone measures on
it:

```ts
await page.measure.setScale(scale); // the whole page is at this scale
await page.measure.setScale(null); // remove it

const { viewports } = await page.measure.listViewports();
```

A page can have several measured regions, called viewports, each with its own scale: a plan and a
detail at another scale on one sheet. `listViewports()` returns them in the order they're drawn:

| Field     | What it is                                       |
| :-------- | :----------------------------------------------- |
| `bbox`    | The region, `{ x, y, width, height }` in points. |
| `measure` | Its scale, or `null`.                            |
| `name`    | Its name, or `null`.                             |
| `owned`   | `true` for the one `setScale()` writes.          |

`setScale()` writes one region of its own that covers the whole page; other apps' regions stay as
they are. When regions overlap, the last one that contains a point is the one for that point:

```ts
import { viewportForPoint } from '@embedpdf/engine';

const region = viewportForPoint(viewports, linePoints.start);

if (region?.measure?.subtype === 'rectilinear') {
  await page.annotations.create({
    subtype: 'line',
    intent: 'line-dimension',
    measure: region.measure,
    linePoints,
  });
}
```

A page's scale isn't applied for you: each measurement carries its own copy of the scale it was
made with. Setting or removing the page's scale leaves existing measurements as they are; to
recalibrate one, update its `measure`. Each change fires a `pages.scaleSet`
[event](/docs/engine/concepts/events).

## Permissions

Measurements are annotations and need the same permissions (see
[Permissions](/docs/engine/concepts/permissions#annotations)). `listViewports()` needs `doc.open`;
`setScale()` needs `doc.annotate.modify`.

## Good to know

### Why a readout has no value

When a readout has no value, `unavailable` is one of these:

| `unavailable`          | Why                                                               |
| :--------------------- | :---------------------------------------------------------------- |
| `'not-dimension'`      | The annotation isn't a measurement (its `intent`).                |
| `'no-measure'`         | It has no scale.                                                  |
| `'foreign-measure'`    | Its scale is of a kind the engine can't compute, such as a map's. |
| `'no-scale'`           | Its scale has no usable conversion.                               |
| `'no-distance-format'` | Its scale says nothing about showing lengths.                     |
| `'no-area-format'`     | Its scale says nothing about showing areas.                       |
| `'invalid-format'`     | Its scale shows numbers in a way that isn't valid.                |
| `'invalid-geometry'`   | Too few points, or an area outline that crosses itself.           |

### Scales the engine can't compute

A map's geospatial scale reads as `{ subtype: 'geospatial' }`, and a scale of a kind the engine doesn't
know as `{ subtype: 'unknown' }`. Both stay in the PDF untouched, give no readout, and can't be
written: a new one is refused with `InvalidArg`. Sending back the one you read keeps it, so an
annotation you read and update keeps its scale. An [import](/docs/engine/annotations/import-export)
leaves such a scale out and reports it (`geospatial`, `unknown-measure`).

### What a scale is

`measure` is the PDF's own rectilinear measure (ISO 32000-2 §12.9): `x` converts points to the
first unit, and `distance` and `area` list how values are shown, one entry per unit of a cascade
such as feet and inches. `ratio` is the text PDF apps show for the scale, such as `"1:100"`. The
helpers build all of it; you rarely need to write one by hand.

### Digits, as Acrobat shows them

A number format's precision is its `/RD`. The standard lets an app drop trailing zeros when the
format doesn't say to keep them; Acrobat shows every digit, and so does the engine, so the same
measurement reads the same in both. A cascade that ends on a whole unit stops there: `2 ft`, not
`2 ft 0 in`.

### Precision

The engine computes with the precision PDF stores numbers in, so a readout you show while the user
draws is exactly the label that's saved.

### Intents in the PDF

The intents are the PDF's `/IT` names, spelled like the engine's other names: `'line-dimension'`
is `/LineDimension`, `'polyline-dimension'` is `/PolyLineDimension` and `'polygon-dimension'` is
`/PolygonDimension`.

---

### Content Engine Annotations Notes And Replies (`docs/content/engine/annotations/notes-and-replies.mdx`)

---
title: Notes & replies
description: Add sticky notes, reply to any annotation, set review states, and show a note's text in a popup.
---

# Notes & replies

A note is the sticky-note icon PDF apps show on a page, with a comment in it. Any annotation can
have replies, so a note, a highlight or a shape can each start a conversation.

```ts
const { annotation: note } = await page.annotations.create({
  subtype: 'text',
  rect: { x: 500, y: 72, width: 20, height: 20 },
  contents: 'Is this total right?',
});

await page.annotations.create({
  subtype: 'text',
  rect: note.rect,
  contents: 'Yes, checked against the invoice.',
  reply: { to: note.ref },
});
```

A note's `subtype` is `'text'`.

## Add a note

| Field      | What it is                                                                               | Default  |
| :--------- | :--------------------------------------------------------------------------------------- | :------- |
| `rect`     | The icon's box, which the icon fills. Required; see [The note icon](#the-note-icon).     |          |
| `contents` | The comment.                                                                             | none     |
| `icon`     | `'note'`, `'comment'`, `'key'`, `'help'`, `'insert'`, `'paragraph'` or `'new-paragraph'` | `'note'` |
| `color`    | The icon's color, `'#rrggbb'`.                                                           | yellow   |
| `open`     | Whether PDF apps show its comment window open; see [Popups](#popups).                    | `false`  |
| `opacity`  | From `0` to `1`.                                                                         | `1`      |
| `subject`  | A short label for the comment, such as `'Question'`.                                     | none     |

Change the comment like any other field, and move the icon by its `rect`:

```ts
await page.annotations.update(note.ref, { contents: 'Is this total right? It looks low.' });
await page.annotations.update(note.ref, { rect: { x: 480, y: 72, width: 20, height: 20 } });
```

## Reply to an annotation

A reply is an annotation with a `reply` field that names the one it answers. It's usually a
note placed where its parent is. Here a note answers a highlight:

```ts
const { x, y } = highlight.rect;

await page.annotations.create({
  subtype: 'text',
  rect: { x, y, width: 20, height: 20 },
  contents: 'Agreed, this needs a source.',
  reply: { to: highlight.ref },
});
```

A reply is on the same page as the annotation it answers; a `to` on another page fails with
`InvalidArg`. You can reply to a reply: it joins the same thread.

To take a reply out of its thread, set `reply: null`. It stays in the document as an annotation
of its own.

## Read a thread

Replies aren't nested inside their parent. Each one is its own annotation in the page's list,
and its `reply.to` is the parent's ref. To find a note's direct replies, compare refs with
`annotationKey()`:

```ts
import { annotationKey } from '@embedpdf/engine';

const { annotations } = await page.annotations.list();
const key = annotationKey(note.ref);

const replies = annotations.filter((a) => a.reply && annotationKey(a.reply.to) === key);
```

For a comments panel, `buildCommentThreads()` does the whole job. It turns a list into threads,
one per annotation that isn't a reply:

```ts
import { buildCommentThreads } from '@embedpdf/engine';

const { annotations } = await page.annotations.list();

for (const thread of buildCommentThreads(annotations)) {
  showComment(thread.root);
  thread.replies.forEach(showReply); // oldest first, replies to replies included
}
```

Each thread has:

| Field          | What it is                                                                         |
| :------------- | :--------------------------------------------------------------------------------- |
| `root`         | The annotation the thread is about.                                                |
| `replies`      | Every reply below it, oldest first. Review states aren't in it.                    |
| `groupedParts` | Parts of the root, such as the strikeout of a suggested replacement (not replies). |
| `review`       | The review states set on it (below).                                               |

Popups, links and form widgets are left out: they're never a comment.

## Review states

A reviewer's verdict, such as _accepted_ or _rejected_, is a reply too: a note with a `state`.
It isn't a comment, so hide its icon:

```ts
await page.annotations.create({
  subtype: 'text',
  rect: note.rect,
  reply: { to: note.ref },
  state: 'accepted',
  hidden: true,
});
```

Each state belongs to a `stateModel`, which the engine sets from the state:

| `state`                                                            | `stateModel` |
| :----------------------------------------------------------------- | :----------- |
| `'accepted'`, `'rejected'`, `'cancelled'`, `'completed'`, `'none'` | `'review'`   |
| `'marked'`, `'unmarked'` (a personal checkmark)                    | `'marked'`   |

Other PDF apps can define models and states of their own; they read back as written. To write
one, give both `state` and `stateModel`: a state the engine doesn't know, without a
`stateModel`, fails with `InvalidArg`.

To change a verdict, add a new state reply rather than editing the old one. Each reviewer's
latest state counts, and `buildCommentThreads()` works that out for you:

```ts
thread.review.lastChange?.state; // the latest verdict, from anyone
thread.review.byReviewer; // each reviewer's latest, by user id (or author name)
thread.review.markedBy; // who has checked it off
```

## Popups

A popup is the window a PDF app opens to show an annotation's comment. It's an annotation of its
own, `subtype: 'popup'`, linked to the one it shows by `parent`:

```ts
const { annotation: popup } = await page.annotations.create({
  subtype: 'popup',
  parent: note.ref,
  rect: { x: 320, y: 92, width: 180, height: 100 },
  open: true,
});
```

| Field    | What it is                                       | Default |
| :------- | :----------------------------------------------- | :------ |
| `parent` | The annotation whose comment it shows. Required. |         |
| `rect`   | Where the window is when it's open. Required.    |         |
| `open`   | Whether PDF apps show the window open.           | `false` |

The engine keeps the link both ways: after this call, the note's `popup` field is the popup's ref.
You never set `popup` yourself. An annotation has one popup: creating a second fails with
`InvalidArg` naming the first. To replace it, delete it and create the new one; to move it, update
its `rect`.

A popup has no text of its own: it shows its parent's `contents`. To open or close it:

```ts
await page.annotations.update(popup.ref, { open: false });
```

A note and its popup hold one `open`: set it on either, and both change (the update's
`meta.changed` names both). A popup made without `open` takes its note's. Other annotations
have no `open` of their own; their popup's is the only one.

You don't need a popup to have a note. Add one when the document should say where the comment
window goes. A note without one can still start open: PDF apps make the window themselves.

## Delete a note

Deleting a note deletes its whole thread, in one change: its replies (and their replies), its
grouped parts, its review states and every popup. Deleting a reply deletes that reply and the
replies under it. This is how Acrobat treats a thread too, and it keeps other viewers from
showing a reply without its note.

```ts
const { meta } = await page.annotations.delete(note.ref);
// meta.changed names everything that went, the note first
```

It's all or nothing: when the user may not delete one of them (someone else's reply, for a user who
may only change their own annotations), nothing is deleted and the call fails with `Forbidden`.
`error.details.refs` names the ones in the way.

## Permissions

A reply, a state and a popup are annotations, so creating one needs permission to create
annotations, and deleting one needs permission to delete it. A user who may only change their
own annotations can still reply to anyone's. See
[Permissions](/docs/engine/concepts/permissions#annotations).

## Good to know

### Groups are parts, not replies

`reply.type` is `'reply'` unless you say otherwise. `'group'` makes an annotation part of
another one, such as the strikeout of a suggested replacement (see
[Highlights & text markup](/docs/engine/annotations/text-markup#suggest-an-insertion-or-a-replacement)).
PDF apps show a group as one annotation, with the comment, author and date of the annotation
its parts point to; the parts' own comments are ignored.

### The note icon

The icon fills `rect`, as in every PDF app: a bigger rect draws a bigger icon, and a rect that
isn't square stretches it. 20 × 20 points is the usual size, the one PDF apps place a note at.
The icon is filled with `color` and outlined in black or white, whichever stands out against it.
Each `icon` draws its own symbol, and a name the engine doesn't know draws `'note'`.

### How it's stored

For readers who know PDF: `reply` is `/IRT` with `/RT`, a popup is linked by the parent's
`/Popup` and the popup's `/Parent`, `open` is `/Open` on the note and on its popup, and states are `/State` and `/StateModel`
on a text annotation, as ISO 32000 describes them. So threads, states and popups made here show
up in other PDF apps, and theirs show up here.

---

### Content Engine Annotations Shapes (`docs/content/engine/annotations/shapes.mdx`)

---
title: Shapes & drawing
description: Draw rectangles, ellipses, lines, arrows, polygons and freehand ink on a page.
---

# Shapes & drawing

Shapes are drawings on top of the page: rectangles, ellipses, lines and arrows, polylines,
polygons and freehand ink. You give the geometry in points, and the engine draws the shape in
the style you choose.

```ts
await page.annotations.create({
  subtype: 'square',
  box: { x: 72, y: 92, width: 200, height: 100 },
  color: '#dc143c',
  strokeWidth: 2,
});
```

Every shape takes the [style fields](/docs/engine/annotations#style): `color`, `opacity`,
`strokeWidth`, `borderStyle`, `dashArray`, and `interiorColor` where the shape has an inside. Left
out, a shape is a solid red line, 1 point wide, with no fill.

## Rectangles and ellipses

`square` draws a rectangle and `circle` an ellipse, inside `box`:

```ts
await page.annotations.create({
  subtype: 'circle',
  box: { x: 300, y: 112, width: 120, height: 80 },
  interiorColor: '#ffeb96',
});
```

| Field             | What it is                                                                        | Default |
| :---------------- | :-------------------------------------------------------------------------------- | :------ |
| `box`             | The box the shape fills. Required.                                                |         |
| `interiorColor`   | The fill, or `null` for none.                                                     | none    |
| `cloudyIntensity` | A cloudy border instead of a straight one; see [Cloudy borders](#cloudy-borders). | none    |
| `rotation`        | Turns the shape, in degrees clockwise; see [Turned shapes](#turned-shapes).       | none    |

The line is drawn inside `box`, so a thick border doesn't make the shape bigger. A read also has
`rect`: where the shape sits on the page, the upright box around all it draws. The engine works
it out; see [The shape's rect](#the-shapes-rect).

## Lines and arrows

A `line` goes from one point to another, with an optional ending at each end:

```ts
await page.annotations.create({
  subtype: 'line',
  linePoints: { start: { x: 100, y: 392 }, end: { x: 320, y: 292 } },
  lineEndings: { start: 'none', end: 'closed-arrow' },
  strokeWidth: 2,
});
```

| Field           | What it is                                                                     | Default                          |
| :-------------- | :----------------------------------------------------------------------------- | :------------------------------- |
| `linePoints`    | `{ start, end }`, two points `{ x, y }`. Required.                             |                                  |
| `lineEndings`   | `{ start, end }`: what's drawn at each end; see [Line endings](#line-endings). | `{ start: 'none', end: 'none' }` |
| `interiorColor` | The fill of closed endings, such as a solid arrowhead.                         | none                             |
| `intent`        | `'line-arrow'` labels the line as an arrow. It changes nothing in the drawing. | none                             |
| `rotation`      | Turns the line, in degrees clockwise; see [Turned shapes](#turned-shapes).     | none                             |

You don't pass a `rect` for a line, a polyline, a polygon or ink: the engine works it out from
what it draws (the points, the line width, the endings) and keeps it up to date when they change;
see [The shape's rect](#the-shapes-rect).

A line with `intent: 'line-dimension'` and a `measure` is a distance measurement; see
[Measurements](/docs/engine/annotations/measurements).

## Polylines and polygons

Both take a list of `vertices`. A `polygon` is closed and can be filled; a `polyline` stays open
and can have endings, like a line:

```ts
await page.annotations.create({
  subtype: 'polygon',
  vertices: [
    { x: 70, y: 332 },
    { x: 170, y: 332 },
    { x: 120, y: 252 },
  ],
  interiorColor: '#c8e6ff',
});

await page.annotations.create({
  subtype: 'polyline',
  vertices: [
    { x: 230, y: 332 },
    { x: 290, y: 252 },
    { x: 350, y: 332 },
  ],
  lineEndings: { start: 'none', end: 'open-arrow' },
});
```

| Field             | What it is                                                                               | Default                          |
| :---------------- | :--------------------------------------------------------------------------------------- | :------------------------------- |
| `vertices`        | The points, in order. Required: at least three for a polygon, two for a polyline.        |                                  |
| `interiorColor`   | A polygon's fill. On a polyline, the fill of closed endings.                             | none                             |
| `lineEndings`     | Polyline only: what's drawn at the first and the last point.                             | `{ start: 'none', end: 'none' }` |
| `cloudyIntensity` | Polygon only: a cloudy border.                                                           | none                             |
| `intent`          | Polygon only: `'polygon-cloud'` labels it as a cloud. It changes nothing in the drawing. | none                             |
| `rotation`        | Turns the shape, in degrees clockwise; see [Turned shapes](#turned-shapes).              | none                             |

A polygon's last point joins its first by itself. Fewer points than a shape needs fail with
`InvalidArg`. Polylines and polygons can also be perimeter and area measurements; see
[Measurements](/docs/engine/annotations/measurements).

## Freehand drawing

An `ink` annotation is what a pen draws: one or more strokes, each a list of points.

```ts
await page.annotations.create({
  subtype: 'ink',
  inkList: [
    [
      { x: 70, y: 712 },
      { x: 120, y: 612 },
      { x: 180, y: 702 },
    ],
    [
      { x: 200, y: 692 },
      { x: 260, y: 632 },
    ],
  ],
  strokeWidth: 3,
});
```

| Field         | What it is                                                                    | Default |
| :------------ | :---------------------------------------------------------------------------- | :------ |
| `inkList`     | The strokes: one list of points per movement of the pen. Required.            |         |
| `strokeWidth` | The pen's width in points, more than `0`: ink has no fill to show otherwise.  | `1`     |
| `intent`      | `'ink-highlight'` labels it as a freehand highlighter.                        | none    |
| `rotation`    | Turns the drawing, in degrees clockwise; see [Turned shapes](#turned-shapes). | none    |

Ink has no fill, and its strokes have round ends. An `inkList` without strokes, or with a stroke
without points, fails with `InvalidArg`. For a highlighter pen, combine
`intent: 'ink-highlight'` with a wide `strokeWidth`, a light `color` and `blendMode: 'multiply'`,
so the text under it stays readable.

## Line endings

`lineEndings` on lines and polylines, and `lineEnding` on [callouts](/docs/engine/annotations/free-text#callouts),
take these values:

| Value              | What's drawn at the end                          |
| :----------------- | :----------------------------------------------- |
| `'none'`           | Nothing.                                         |
| `'open-arrow'`     | An arrowhead of two lines, pointing outward.     |
| `'closed-arrow'`   | A triangle arrowhead, pointing outward.          |
| `'r-open-arrow'`   | An open arrowhead, pointing back along the line. |
| `'r-closed-arrow'` | A triangle arrowhead, pointing back.             |
| `'square'`         | A square.                                        |
| `'circle'`         | A circle.                                        |
| `'diamond'`        | A diamond.                                       |
| `'butt'`           | A short line across the end.                     |
| `'slash'`          | A short slanted line across the end.             |

The closed ones (`'closed-arrow'`, `'r-closed-arrow'`, `'square'`, `'circle'`, `'diamond'`) are
filled with `interiorColor` when you set one, and outlined when you don't. Endings grow with the
line: an arrowhead is 9 times the `strokeWidth` long.

## Dashed lines

Set `borderStyle: 'dashed'`, and the pattern in `dashArray` if you want your own: lengths in
points, of line then gap.

```ts
await page.annotations.update(shape.ref, { borderStyle: 'dashed', dashArray: [6, 3] });
```

`[6, 3]` draws 6 points of line, then 3 points of gap. Without a `dashArray`, dashes are 3 points
on, 3 off. A `dashArray` on a `'solid'` border is kept, and draws when the border becomes dashed.

## Cloudy borders

Squares, circles and polygons can have a cloudy border instead of a straight one, the bumpy outline
reviewers draw around a part of a drawing. `cloudyIntensity` sets how big the bumps are, usually
`1` or `2`; `null` makes the border straight again.

```ts
await page.annotations.create({
  subtype: 'square',
  box: { x: 100, y: 412, width: 120, height: 80 },
  cloudyIntensity: 2,
  strokeWidth: 2,
});
```

The bumps stick out past the box. The engine makes room for them in `rect`, and `box` stays the
shape itself.

## Permissions

Reading shapes needs `doc.annotate.read`. Creating, changing and deleting them needs the same
permissions as any annotation; see [Permissions](/docs/engine/annotations#permissions).

## Good to know

### Turned shapes

A rectangle or an ellipse can be turned by any angle. Give the shape's own box, before turning, as
`box`, and the angle as `rotation`, in degrees clockwise (as a page turns, and as CSS `rotate()`
turns):

```ts
await page.annotations.create({
  subtype: 'square',
  box: { x: 100, y: 412, width: 160, height: 80 },
  rotation: 30,
});
```

The shape turns about the middle of its box. The engine sets `rect` to the upright box around the
turned shape, which is what a PDF app uses to find it. To move or resize a turned shape, change
its `box`; to straighten it where it is, set `rotation: null`. Free text boxes, carets and stamps
turn the same way.

At a quarter turn (90, 180 or 270), you can create a shape by where it stands instead: its `rect`
and `rotation`. The engine works out the `box`, which is the `rect` with its sides swapped under
90 and 270:

```ts
// A 160 × 40 box, standing 40 wide and 160 tall
await page.annotations.create({
  subtype: 'square',
  rect: { x: 100, y: 100, width: 40, height: 160 },
  rotation: 90,
});
```

At any other angle, give the `box`: many boxes stand in the same `rect`, so a create by `rect`
fails with `InvalidArg`. A create with both takes the `box`, as when you pass a read back.

Lines, polylines, polygons and ink turn the same way too. Give their points upright, and the angle
as `rotation`; they turn about the middle of the box around the points:

```ts
await page.annotations.create({
  subtype: 'polygon',
  vertices: [
    { x: 100, y: 492 },
    { x: 260, y: 492 },
    { x: 180, y: 412 },
  ],
  rotation: 45,
});
```

A read gives the points back upright, with the turn. `drawnPointsOf(annotation)` gives them as
the page shows them: one list for a line or a polygon, one per ink stroke. To move a turned shape,
move its points; to straighten it where it is, set `rotation: null`. A measurement's
[caption](/docs/engine/annotations/measurements) turns with its shape.

```ts
import { drawnPointsOf } from '@embedpdf/engine';

const [shown] = drawnPointsOf(polygon)!; // the corners where the page shows them
```

### The shape's rect

`rect` is where the shape sits on the page: the upright box around all it draws, turned, with
what its border adds (a cloud's bumps) and, for lines, the line width and the endings. The engine
works it out from `box` or the points, `rotation` and the style. The `rect` you read, sent back,
changes nothing, so a read passed straight back works. A different `rect` moves or stretches the
shape into it, as in Acrobat; see
[Move or resize by `rect`](/docs/engine/annotations#move-or-resize-by-rect). You can also change
the `box` or the points yourself.

### Border styles

A shape from another PDF app can read `'beveled'` or `'inset'`. Only form widgets draw those, so
the engine draws such a shape with a solid line and keeps the style when you send it back
unchanged; a write that sets one fails with `InvalidArg`. A cloudy border is never dashed.

### How it's stored

For readers who know PDF: `linePoints` is `/L`, `vertices` is `/Vertices`, `inkList` is
`/InkList`, `lineEndings` is `/LE`, `interiorColor` is `/IC` and `cloudyIntensity` is the
intensity of a `/BE` cloudy border effect. For a cloudy shape the PDF's own `/Rect` takes in the
bumps and `/RD` says where the shape is inside it, as Acrobat writes it; the engine writes both
from `box`. A turned shape keeps its box in `/EMBD_Metadata`, and its drawing is turned inside its
`/Rect`, so every PDF app shows it turned.

A turned line, polyline, polygon or ink keeps its points turned in the file, as every PDF app
reads them, and its turn and the box around its upright points in `/EMBD_Metadata`. When another
app moves or redraws the points, the two no longer match: a read then gives the points as drawn,
with `rotation: null`.

---

### Content Engine Annotations Stamps (`docs/content/engine/annotations/stamps.mdx`)

---
title: Stamps
description: Place an image or a PDF drawing on a page, turn and resize it, and copy it to another page or document.
---

# Stamps

A stamp puts a picture on a page: a logo, a signature, an "Approved" mark. You give the engine the
picture as PNG, JPEG or PDF bytes, and the box to put it in:

```ts
const image = await (await fetch('/approved.png')).blob();

const { annotation } = await page.annotations.create(
  { subtype: 'stamp', box: { x: 72, y: 102, width: 150, height: 50 } },
  { resources: { appearance: image } },
);
```

The picture goes in the options, as `resources.appearance`. A `Blob` (such as a file the user
picked) and a `Uint8Array` both work.

| Field      | What it is                                                                           | Default     |
| :--------- | :----------------------------------------------------------------------------------- | :---------- |
| `box`      | The box the stamp fills, `{ x, y, width, height }` in points, before any turn        | required    |
| `fit`      | How the picture fills the box: `'contain'`, `'cover'` or `'fill'`                    | `'contain'` |
| `opacity`  | From `0` (invisible) to `1` (solid), over the whole picture                          | `1`         |
| `name`     | A label for the stamp, such as `'Approved'`                                          | none        |
| `rotation` | How far the stamp is turned, in degrees clockwise; see [Turn a stamp](#turn-a-stamp) | none        |

A read also has `rect`, where the stamp sits on the page: its box, or the upright box around it
when it's turned. The engine works it out.

`fit` works like CSS `object-fit`: `'contain'` shows the whole picture at its own proportions,
`'cover'` fills the box at its own proportions and crops what sticks out, `'fill'` stretches the
picture to the box. The stamp remembers its `fit`, so a later resize fills the new box the same
way.

## Images and PDF drawings

| Bytes | What you get                                                    |
| :---- | :-------------------------------------------------------------- |
| PNG   | The image, with its transparency.                               |
| JPEG  | The image, stored as it is, without re-encoding.                |
| PDF   | The page as a vector drawing, sharp at any zoom. One page only. |

The engine tells which it is from the bytes themselves, never from a file name or a MIME type.
Anything else, a PDF with more than one page included, is refused with `InvalidArg` before
anything is written. So is a stamp without an `appearance`.

### A page of a PDF

To use a page of an open document as a stamp, extract it: `pages.extract()` returns a one-page
PDF.

```ts
const drawing = await doc.pages.extract([pageRef]);

await page.annotations.create({ subtype: 'stamp', box }, { resources: { appearance: drawing } });
```

### Standard stamps

The engine has no stamps of its own. `name` is only a label stored with the stamp; the picture is
always the `appearance` you give it.

<Engine local>

The standard set (Approved, Draft, Confidential, Sign Here and more) ships as vector drawings in
`@embedpdf/default-stamps`: one PDF per language, one page per stamp. Each page is registered
under a name `identifier=label`, such as `Approved=Goedgekeurd` in the Dutch file:

```ts
import { loadDefaultLibrary } from '@embedpdf/default-stamps/library';

const library = await engine.open({ kind: 'bytes', bytes: await loadDefaultLibrary('en') });
const { namedPages } = await library.pages.list();
const approved = namedPages.find((entry) => entry.name.startsWith('Approved='));

if (approved?.target.kind === 'page') {
  const drawing = await library.pages.extract([approved.target.page]);
  await page.annotations.create(
    { subtype: 'stamp', box, name: 'Approved', subject: 'Approved' },
    { resources: { appearance: drawing } },
  );
}
await library.close();
```

The identifier becomes the stamp's `name` and the label its `subject`, the way Acrobat writes
stamps from its own libraries. In a viewer, the [stamp plugin](/docs/headless/annotations/stamps)
does this for you.

</Engine>

## Turn a stamp

A stamp turns like a rectangle: give its own box as `box` and the angle as `rotation`, in degrees
clockwise.
See [Turned shapes](/docs/engine/annotations/shapes#turned-shapes).

```ts
const { annotation } = await page.annotations.create(
  {
    subtype: 'stamp',
    box: { x: 300, y: 442, width: 100, height: 50 }, // 100 × 50
    rotation: 90,
  },
  { resources: { appearance: image } },
);

annotation.rect; // { x: 325, y: 417, width: 50, height: 100 }: the same box, turned
```

A stamp turned in Acrobat reads the same way: its `rotation`, and its own box as `box`. A stamp
you turn opens in Acrobat as a turned stamp.

## Resize and refit

A new `box` or a new `fit` places the same picture again. Nothing is uploaded or re-encoded:

```ts
await page.annotations.update(annotation.ref, {
  box: { x: 72, y: 102, width: 300, height: 90 },
});
await page.annotations.update(annotation.ref, { fit: 'cover' });
```

The stamp keeps its whole picture, whatever box it's in, so a stamp set from `'cover'` back to
`'contain'` shows all of it again.

To change the picture itself, pass a new `resources.appearance`:

```ts
await page.annotations.update(annotation.ref, {}, { resources: { appearance: otherImage } });
```

A new `opacity` replaces the old one, and applies to the picture as a whole: overlapping parts of
the artwork don't show through each other.

## Download a stamp's drawing

A read never contains the picture. `downloadResource()` returns it, as a one-page PDF:

```ts
const drawing = await page.annotations.downloadResource(annotation.ref, 'appearance');
```

The drawing is the picture before the stamp's `fit`, `rotation` and `opacity`. For a stamp made
from a PNG or JPEG, it's the image at its own size, one pixel to a point, whatever box the stamp
is in. For a stamp made by another app, it's the picture as that app drew it.

## Copy a stamp

The read and the drawing together make the same stamp again, on another page or in another
document:

```ts
const drawing = await page.annotations.downloadResource(stamp.ref, 'appearance');

await otherPage.annotations.create(stamp, { resources: { appearance: drawing } });
```

The copy has the same box, `fit`, `rotation`, `opacity` and `name`; the engine records the user
who makes it as its author.

To copy on the same page, clear the unique name: `{ ...stamp, nm: null }` (see
[Refs & identity](/docs/engine/concepts/identity)). A copy with a name that's taken on its page
fails with `InvalidArg`.

To copy many annotations at once, stamps included, use [Import & export](/docs/engine/annotations/import-export).

## Permissions

Stamps are annotations: creating, changing and deleting them needs the same permissions as any
annotation (see [Permissions](/docs/engine/concepts/permissions#annotations)). Downloading a
drawing sends document content out, so it needs `doc.download`, and so does `pages.extract()`.

## Good to know

### Shared drawings

Stamps with the same picture share one drawing, so a logo placed 100 times is stored once, also
after a save, and when you download it and place it again.

Changing one stamp never changes the others. A new picture for one stamp gives that stamp a
drawing of its own; the stamps that shared the old one keep it.

### The same drawing is the same bytes

A drawing always downloads as the same bytes: from the local and the cloud engine, and again
after a copy. So you can store drawings by their hash and keep one copy for every stamp that uses
it.

### Stamps from other apps

A stamp made by Acrobat or another app records no fit and reads `fit: null`. A resize stretches
its picture to the new box, as every PDF app shows it. A stamp from another app keeps its saved
opacity through a resize or copy; a new `opacity` replaces it. The same goes the other way:
Acrobat can change the opacity of a stamp made here.

### Large images

An image becomes a drawing at its own size, one pixel to a point, scaled down to fit 14,400
points (Acrobat's largest page size). The stamp's box then scales it to where it goes.

### The name

`name` takes any text that isn't empty: a standard stamp name such as `'Approved'`, or the id
Acrobat gives a stamp from a custom library. In the PDF it's the stamp's `/Name`.

---

### Content Engine Annotations Text Markup (`docs/content/engine/annotations/text-markup.mdx`)

---
title: Highlights & text markup
description: Highlight, underline, squiggly-underline or strike out text on a page, from a search match or a selection.
---

# Highlights & text markup

Text markup marks text that is already on the page: a highlight, an underline, a squiggly
underline or a strikeout. You tell the engine where the text is, and it draws the mark over it.

```ts
const { matches } = await doc.search.query({ text: 'total amount' });

for (const match of matches) {
  await doc.page(match.page).annotations.create({
    subtype: 'highlight',
    quadPoints: match.segments.map((segment) => segment.quad),
  });
}
```

## The four kinds

| `subtype`     | What it draws                  | Default color |
| :------------ | :----------------------------- | :------------ |
| `'highlight'` | A marker over the text         | yellow        |
| `'underline'` | A straight line under the text | black         |
| `'squiggly'`  | A wavy line under the text     | black         |
| `'strikeout'` | A line through the text        | black         |

All four take the same fields. To underline instead of highlight, change only the `subtype`.

## Mark a search match

A search match has one segment per line it covers, and each segment has a `quad`: the four
corners of that piece of text. `quadPoints` takes exactly those quads, so a match becomes a
highlight in one call, as above. See [Search](/docs/engine/text/search).

## Mark a selection

For text the user selects, get the quads from the page's text layout:

```ts
const layout = await page.text.layout();
const range = layout.wordAt(point); // { start, count }, or null

if (range) {
  await page.annotations.create({
    subtype: 'underline',
    quadPoints: layout.segments(range).map((segment) => segment.quad),
  });
}
```

Any range works: a word, a line, or everything between two characters of a drag. See
[Selection & geometry](/docs/engine/text/selection-and-geometry) for how to get one.

A markup sits on one page. When a selection runs over two pages, create one markup on each page,
each from that page's range.

## Fields

| Field        | What it is                                                          | Default                                             |
| :----------- | :------------------------------------------------------------------ | :-------------------------------------------------- |
| `quadPoints` | Where the text is: one quad per line piece. Required, at least one. |                                                     |
| `color`      | The mark's color, `'#rrggbb'`.                                      | yellow for a highlight, black for the others        |
| `opacity`    | From `0` (invisible) to `1` (solid).                                | `1`                                                 |
| `blendMode`  | How the color mixes with the page under it.                         | `'multiply'` for a highlight, `'normal'` for others |
| `contents`   | A comment, which PDF apps show with the mark.                       | none                                                |

You don't pass a `rect`: the engine works it out from the quads, as the box around all of them.
In an update, a different `rect` moves the quads into it; see
[Move or resize by `rect`](/docs/engine/annotations#move-or-resize-by-rect).
The fields every annotation has (flags, `subject`, who wrote it) are on the
[overview](/docs/engine/annotations).

## Color

```ts
await page.annotations.update(highlight.ref, { color: '#90ee90' });
```

A highlight multiplies its color with the page, like a real marker: the text under it stays dark
and readable, whatever the color. Pass `blendMode: 'normal'` to paint the color over the text
instead, and lower `opacity` to let the text show through.

## Change what it covers

Send new quads to move a mark to other text. They replace the old ones:

```ts
await page.annotations.update(highlight.ref, {
  quadPoints: layout.segments(newRange).map((segment) => segment.quad),
});
```

## Keep the text it marks

A markup stores where the text is, not which characters it covers. If you need the text later,
for a list of highlights or an export, read it when you create the mark and keep it in
`contents`:

```ts
const text = await page.text.slice(range);

await page.annotations.create({
  subtype: 'highlight',
  quadPoints: layout.segments(range).map((segment) => segment.quad),
  contents: text,
});
```

PDF apps show `contents` as the highlight's comment, so the quoted text shows up there too.

## Suggest an insertion or a replacement

Reviewers mark a change to the text with a caret: `subtype: 'caret'` at the spot where text
should go, with the new text in `contents`. Its `box` is the box the caret symbol is drawn in.

```ts
await page.annotations.create({
  subtype: 'caret',
  box: { x: 208, y: 82, width: 8, height: 12 },
  contents: 'not ',
});
```

To suggest replacing text, strike out the old text and put a caret right after it. Link the two,
so PDF apps treat them as one suggestion:

```ts
const { annotation: caret } = await page.annotations.create({
  subtype: 'caret',
  intent: 'replace',
  box: { x: 208, y: 82, width: 8, height: 12 },
  contents: 'invoice',
});

await page.annotations.create({
  subtype: 'strikeout',
  intent: 'strikeout-text-edit',
  quadPoints: layout.segments(range).map((segment) => segment.quad),
  reply: { to: caret.ref, type: 'group' },
});
```

The caret holds the suggestion: its `contents`, its author and its replies. The strikeout is a
part of it. A caret is red by default and takes `color` and `opacity` like a markup.

## Permissions

Creating a markup needs permission to create annotations; see
[Permissions](/docs/engine/concepts/permissions#annotations). Finding the text needs less than
reading it: the layout needs `doc.text.select` and a search needs `doc.text.search`. Only
`page.text.slice()` needs `doc.text.copy`. So a user may highlight text they aren't allowed to
copy.

## Good to know

### Quads

A quad is four corners, `{ upperLeft, upperRight, lowerLeft, lowerRight }`, named as the text
itself stands: `upper` is the top of the letters, and `left` is the left end as it would be if the
text stood upright. On text turned upside down, `upperLeft` is at the bottom right of the page.
Right-to-left text uses the same names. Segments from a search and from the layout carry these
names, for upright, rotated and vertical text alike, so pass them unchanged.

Other PDF apps store a markup's corners in different orders. The engine sorts them out when it
reads the file, so every quad it gives you uses these names.

### One mark, many lines

A highlight over three lines is one annotation with three quads, not three annotations. PDF apps
select, move and delete it as one.

### Replies and popups

A markup can have replies and a popup, just like a note: see
[Notes & replies](/docs/engine/annotations/notes-and-replies). For readers who know PDF: the
suggestion link is `/IRT` with `/RT /Group`, and the two intents are `/IT /Replace` and
`/IT /StrikeOutTextEdit`.

---

### Content Engine Concepts Coordinates (`docs/content/engine/concepts/coordinates.mdx`)

---
title: Coordinates
description: Where the engine measures positions from, which way angles turn, how colors are spelled, and how to meet tools that use the PDF file's own numbers.
---

# Coordinates

Every position the engine gives or takes is in **page space**: annotations, text, search matches,
render areas, form widgets, measurement regions and link destinations. You can pass a value from
one call straight to another, and to the screen.

## The rules

1. **Positions** are points (1/72 inch) from the top-left corner of the page, with y going down,
   as on a screen.
2. **Shapes:** a point is `{ x, y }`, a box is `{ x, y, width, height }` with `x, y` its top-left
   corner, and a quad is four named corners: `upperLeft`, `upperRight`, `lowerLeft` and
   `lowerRight`.
3. **Angles** are degrees, clockwise as you see the page, as CSS `rotate()` turns.
4. **Colors** are `'#rrggbb'`, as in CSS: lowercase when the engine gives one, either case when it
   takes one. Transparency is the separate `opacity`.

## Which page, and which corner

"The page" is what the page shows: its crop box, cut back to its media box where it reaches past
it. `page.size` is that area's width and height, and `boxes.crop` is always
`{ x: 0, y: 0, width, height }`. A file whose media box starts at a negative corner, or whose crop
box sits inside the page, still has its top-left at `0, 0`. See [Pages](/docs/engine/documents/pages).

Positions are on the page as the file stores it, before the page's `rotation`. A page shown turned
by 90° keeps its numbers; renders and the page transform apply the turn for you.

A link's destination is measured on the page it goes to, not on the page the link sits on.

## An example with numbers

A file stores a page with a media box of `[0 0 612 792]` and a crop box of `[50 60 562 732]`. The
page shows the crop box: 512 × 672 points. In the file's own numbers, y goes up from the bottom of
the media box, so its top-left corner is at `50, 732`.

A highlight the file stores at `left 100, bottom 600, right 150, top 650` reads as:

```ts
highlight.rect; // { x: 50, y: 82, width: 50, height: 50 }
```

`x` is 100 − 50: from the left edge of what the page shows. `y` is 732 − 650: from its top edge,
down to the highlight's top.

## What never changes

- **Lengths** are the same numbers in every space: `strokeWidth`, `dashArray`, font sizes, a
  measurement's `leader` lengths and `captionOffset`, `cloudyIntensity`.
- **Sides along a line.** A positive `leader.length`, and a positive
  `captionOffset.perpendicular`, point to the left of the line as you go from its start to its
  end, as the page shows it: above a line drawn from left to right. `captionOffset.along` runs
  from the start toward the end.
- **Order.** The points of a polygon, a polyline or an ink stroke keep the order they have in
  the file.
- **Quad corners** are named as the text stands, not as the page shows it: `upperLeft` and
  `upperRight` are the top edge of the letters, left to right as the text would sit upright. On
  text turned 180°, `upperLeft` is at the bottom right of the page. A quad another app wrote gets
  the same names when the engine reads it.

## Colors in the file

A PDF can store a color as gray, RGB or CMYK. It reads as the same color in `'#rrggbb'`. A color
you send back unchanged stays as the file has it, so passing an annotation you read straight back
to `update()` rewrites nothing. A color you change is written as RGB.

## Tools that use the file's numbers

Tools that work on the file itself, such as iText, PDFBox or Acrobat JavaScript, use the file's
own numbers: y goes up, a box is `{ left, bottom, right, top }`, and a page can sit anywhere in
them. Each page's `pdfCropBox` says where the page sits, and the engine converts both ways:

```ts
import { pageBoxOf, pdfRectOf } from '@embedpdf/engine';

const inTheFile = pdfRectOf(highlight.rect, page.pdfCropBox);
// { left: 100, bottom: 600, right: 150, top: 650 }
const onThePage = pageBoxOf(inTheFile, page.pdfCropBox);
// { x: 50, y: 82, width: 50, height: 50 }
```

| Page space → the file's numbers | The file's numbers → page space |
| :------------------------------ | :------------------------------ |
| `pdfPointOf(point, box)`        | `pagePointOf(point, box)`       |
| `pdfRectOf(box, box)`           | `pageBoxOf(rect, box)`          |
| `pdfQuadOf(quad, box)`          | `pageQuadOf(quad, box)`         |
| `pdfDestinationOf(dest, box)`   | `pageDestinationOf(dest, box)`  |

For a destination, pass the `pdfCropBox` of the page it goes to.

## Pixels

Pixels depend on the zoom and the turn, so the engine never gives or takes them. Every rendered
image has the transform it was drawn with, which converts between its pixels and page space; see
[Rendering](/docs/engine/documents/rendering#from-image-pixels-to-page-points).

---

### Content Engine Concepts Errors And Cancelling (`docs/content/engine/concepts/errors-and-cancelling.mdx`)

---
title: Errors & cancelling
description: Every failure is an EngineError with a code you can branch on, and every call can be cancelled.
---

import { Callout } from '@/components/docs/callout';

# Errors & cancelling

Every call that fails rejects with an `EngineError`. Its `code` says what went wrong, so you can
handle each case:

```ts
import { EngineError, EngineErrorCode } from '@embedpdf/engine';

try {
  await page.annotations.update(ref, { contents: 'Done' });
} catch (error) {
  if (EngineError.is(error, EngineErrorCode.Forbidden)) {
    showMessage('You can only edit your own comments.');
  } else if (EngineError.is(error, EngineErrorCode.NotFound)) {
    showMessage('This comment was deleted.');
  } else {
    throw error;
  }
}
```

`EngineError.is(error, code)` checks both that it's an engine error and that it has that code.
Branch on the code, never on the message: messages are for people and can change.

## Cancelling a call

Every call returns a promise you can cancel. Keep it, and call `abort()` when you no longer need
the answer:

```ts
const rendering = page.render.image({ viewport: { kind: 'width', width: 1600 } });

// The user scrolled past the page:
rendering.abort();
```

The promise rejects right away with code `Aborted`, and the engine stops the work if it hasn't
finished. Cancelling a call that already finished does nothing.

```ts
try {
  const image = await rendering;
} catch (error) {
  if (EngineError.is(error, EngineErrorCode.Aborted)) return; // we cancelled it
  throw error;
}
```

Only the promise the call returned has `abort()`: `rendering.then(…)` gives a plain promise. Keep
the original.

A time limit is a cancel on a timer:

```ts
const listing = doc.annotations.list();
const timer = setTimeout(() => listing.abort(), 5000);

try {
  return await listing;
} finally {
  clearTimeout(timer);
}
```

When the rest of your code cancels with an `AbortController` (a `fetch`, a framework's cleanup),
tie the call to its signal with `abortWith()`:

```ts
const controller = new AbortController();

const image = await page.render.image().abortWith(controller.signal);

// later, in your cleanup:
controller.abort();
```

The call is cancelled when the signal aborts, or at once if it already has.

## Error codes

The codes you'll handle most:

| Code                      | What happened                                                                                                                                        |
| :------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Forbidden`               | The user doesn't have the permission. `error.details.required` names it.                                                                             |
| `NotFound`                | The page, annotation or field isn't there (anymore).                                                                                                 |
| `InvalidArg`              | Something you passed isn't valid. The message says what.                                                                                             |
| `Aborted`                 | You cancelled the call.                                                                                                                              |
| `DocPasswordRequired`     | The file needs a password. See [Opening documents](/docs/engine/documents/opening#password-protected-files).                                         |
| `DocPasswordIncorrect`    | `unlock()` got a wrong password.                                                                                                                     |
| `DocOpenFailed`           | The file isn't a PDF, or is too damaged to open.                                                                                                     |
| `PayloadTooLarge`         | An import or export is past a limit. `error.details` has the `limit` and `max`.                                                                      |
| `ProtectedDocument`       | A signature in the document forbids this change.                                                                                                     |
| `ObjectNumberUnavailable` | A create named an object number this session doesn't hold, or used one twice.                                                                        |
| `ChangeConflict`          | A change's `expect` didn't match the document. Nothing was written. See [Changes & undo](/docs/engine/documents/changes#guard-a-change-with-expect). |
| `UndoUnavailable`         | The change can't be undone anymore. `error.details.reason` says why: `'final-change'` or `'expired'`.                                                |
| `LayerFull`               | The document has as many objects as a PDF file should; a compacted copy starts afresh.                                                               |

<Engine cloud>

On the cloud you'll also meet:

| Code                    | What happened                                                          |
| :---------------------- | :--------------------------------------------------------------------- |
| `Unauthenticated`       | The token is missing, invalid or expired.                              |
| `SharePasswordRequired` | The share link needs its passphrase, or the one you gave was wrong.    |
| `Network`               | The request didn't reach the server (offline, DNS, TLS).               |
| `LayerVersionConflict`  | Someone else saved at the same moment. Nothing was changed; try again. |

When the server is busy or restarting, the engine retries for you before any of these reach you;
see [Setup](/docs/engine/setup#busy-servers).

</Engine>

## Good to know

### Every code

| Code                     | What happened                                                              |
| :----------------------- | :------------------------------------------------------------------------- |
| `DocNotOpen`             | The document was closed.                                                   |
| `RuntimeUnavailable`     | The engine was destroyed, or the browser lacks something it needs.         |
| `MalformedPdf`           | The file opened, but part of it is too damaged to use.                     |
| `NotImplemented`         | This engine can't do this (yet).                                           |
| `SigningPending`         | A signature is being made; finish or cancel it first.                      |
| `SigningExpired`         | The signature took too long; start it again.                               |
| `SigningVersionMismatch` | The document changed while the signature was being made; start it again.   |
| `SignatureRefused`       | This field can't be signed. The message says why.                          |
| `StaleBase`              | The document was signed elsewhere; this copy can be edited but not signed. |
| `IdempotencyKeyReused`   | An `opId` that already named another change was used again.                |
| `WireFormat`             | The server sent something the engine didn't expect.                        |
| `Unknown`                | Anything else. Please report it.                                           |

### `details`

Some errors carry more in `error.details`: the missing permission for `Forbidden`, the field for
`InvalidArg`, the limit for `PayloadTooLarge`. Log `error.toJSON()` to see everything an error
has.

---

### Content Engine Concepts Events (`docs/content/engine/concepts/events.mdx`)

---
title: Events
description: Get told when a document changes, whoever changed it, and keep your own state in sync.
---

import { Callout } from '@/components/docs/callout';

# Events

Every change to a document produces an event: an annotation created, a page rotated, a field
filled in. Listen to them to keep your interface in sync without reading the document again.

```ts
const stop = doc.events.on('annotations.created', (event) => {
  addToSidebar(event.annotation);
});

// later
stop();
```

`on()` returns a function that stops listening. To hear every event, use `subscribe()`:

```ts
const stop = doc.events.subscribe((event) => {
  console.log(event.type);
});
```

## What an event contains

An event carries exactly what the call that made the change returned, plus its `type` and where
it came from (`origin`). The event for `create()` has the same `annotation` that `create()`
resolved with:

```ts
doc.events.on('annotations.updated', ({ annotation, origin }) => {
  if (origin.kind === 'remote') showToast(`${annotation.author} changed a comment`);
});
```

| Field           | What it is                                                                        |
| :-------------- | :-------------------------------------------------------------------------------- |
| `origin.kind`   | `'local'` when the change was made through this `doc`, `'remote'` when it wasn't. |
| `origin.sub`    | The user who made it (on the cloud).                                              |
| `origin.ts`     | When it happened, in milliseconds.                                                |
| `origin.tx`     | The write it came from: its `opId`, and its place among that write's events.      |
| `origin.undoOf` | On an undo's events: the `opId` of the change it undid.                           |

An event fires after the change is saved, never before, and before the call's promise resolves.
By the time `await page.annotations.create(…)` returns, your listeners have run.

## Every event

| Type                                                                      | When                                                                                      |
| :------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------- |
| `annotations.created`, `.updated`, `.deleted`                             | An annotation changed.                                                                    |
| `annotations.reordered`                                                   | A page's annotations were restacked. `order` is the page's new order, bottom to top.      |
| `annotations.restored`                                                    | An undo brought back deleted annotations.                                                 |
| `annotations.flattened`                                                   | Annotations were baked into a page.                                                       |
| `pages.reordered`, `.rotated`, `.deleted`, `.inserted`                    | The page order or rotation changed.                                                       |
| `pages.named`                                                             | A page name was added or removed.                                                         |
| `pages.flattened`                                                         | All annotations of pages were baked in.                                                   |
| `pages.scaleSet`                                                          | A page's measurement scale changed.                                                       |
| `forms.valueSet`                                                          | A field was filled in.                                                                    |
| `forms.created`, `.updated`, `.deleted`, `.widgetAdded`, `.widgetRemoved` | A field's structure changed.                                                              |
| `forms.widgetUpdated`, `forms.widgetsReordered`                           | A widget was moved or restyled, or a page's widgets were restacked.                       |
| `forms.widgetDeleted`, `forms.widgetRestored`                             | A widget was deleted, or an undo brought it back.                                         |
| `forms.calculationsReordered`                                             | The order calculate scripts run in changed.                                               |
| `forms.restored`                                                          | An undo brought back a deleted field.                                                     |
| `forms.repaired`                                                          | Many values or fields changed at once.                                                    |
| `attachments.created`, `.deleted`                                         | An attached file was added or removed.                                                    |
| `attachments.restored`                                                    | An undo brought back a deleted attached file.                                             |
| `metadata.updated`                                                        | The title, author or another standard metadata field.                                     |
| `metadata.customUpdated`                                                  | Your own metadata keys.                                                                   |
| `outline.created`, `.updated`, `.moved`, `.deleted`                      | An outline item was added, changed, moved or deleted.                                     |
| `outline.restored`                                                        | An undo brought back deleted outline items.                                               |
| `redaction.applied`                                                       | Redactions were applied.                                                                  |
| `signatures.prepared`, `.completed`, `.cancelled`                         | A signature was started or finished.                                                      |
| `document.versioned`                                                      | A new version of the document was saved, such as after signing. Read what you show again. |
| `stream.desynced`                                                         | (cloud) Events were missed. Read what you show again.                                     |

Changes that touch one page (annotations) carry that `page`, so you can update only what's on
screen.

## Which write an event came from

Every write takes an `opId` in its options, a name for the write, and every event it fires carries
it as `origin.tx.id`. Leave it out and the engine makes one up. That's how you match an event to a
change you made:

```ts
const opId = crypto.randomUUID();
doc.events.on('annotations.updated', ({ origin }) => {
  if (origin.tx?.id === opId) markSaved();
});
await page.annotations.update(ref, { contents: 'Seen' }, { opId });
```

An `opId` is 1 to 255 visible ASCII characters (what an HTTP header can carry), on every engine.
On the cloud, a write retried with the same `opId` is applied once, and the retry gets the first
answer. The cloud engine retries a write lost on the network itself, under the same `opId`, so use
a fresh `opId` for every write. A [change](/docs/engine/documents/changes#asking-again), and the
annotation, form and metadata calls a change can hold, do the same on the local engine.

The `opId` is also how you undo a write: `doc.apply({ undoOf: opId })`. See
[Changes & undo](/docs/engine/documents/changes).

Some calls make many changes at once. An [import](/docs/engine/annotations/import-export) of 50
annotations fires 50 `annotations.created` events, one after the other, all with the same
`origin.tx`:

```ts
doc.events.on('annotations.created', ({ annotation, origin }) => {
  addToSidebar(annotation);
  if (origin.tx && origin.tx.index === origin.tx.count - 1) redrawSidebar(); // the last of the batch
});
```

`tx.id` is the same for all of them, and `tx.index` counts from `0` to `tx.count - 1`. A write
that makes one change fires one event, with `tx.index` `0` and `tx.count` `1`.

<Engine local>

## Who hears what

The local engine tells you about changes made through this engine. Two tabs with the same file
are two separate copies: a change in one doesn't reach the other.

<CloudPdfCallout
  title="Live changes from everyone"
  href="https://www.cloudpdf.com/docs/engine/concepts/events"
  cta="Collaborate with CloudPDF"
>

With CloudPDF, everyone who has the document open hears every change as it's saved, with
`origin.kind: 'remote'` and the user who made it. Add a comment in one browser and it appears in
the others, through the same `events.on()` you already use.

</CloudPdfCallout>

</Engine>

<Engine cloud>

## Other people's changes

Everyone who has the document open hears every change as the server saves it. Changes made
through this `doc` arrive with `origin.kind: 'local'`; everyone else's with `'remote'` and the user
in `origin.sub`. Every `open()` is someone else here, even by the same user: a second tab, or the
same document opened twice in one app (a preview beside an editor). Their changes arrive as
`'remote'`, with the same `sub`.

The engine connects to the server when you add the first listener and disconnects after you
remove the last, so listening is free until you need it.

If the connection drops and the engine can't catch up with everything that happened in the
meantime, you get a `stream.desynced` event. Read what you show again (`doc.annotations.list()`,
`doc.pages.list()`); events continue from there.

</Engine>

## Good to know

- **Each change arrives once.** Your own changes aren't sent back to you a second time.
- **A listener that throws** doesn't stop the other listeners, and doesn't make the call fail.
- **`origin.kind` is about the open document, not the user.** "Did I do this?" (undo, optimistic
  UI): check `origin.kind`. "Did this user do it?": check `origin.sub`.

---

### Content Engine Concepts Identity (`docs/content/engine/concepts/identity.mdx`)

---
title: Refs & identity
description: What a page or annotation ref contains, how long it stays valid, and how to store one.
---

import { Callout } from '@/components/docs/callout';

# Refs & identity

Pages and annotations are addressed by a **ref**: `page.ref`, `annotation.ref`. The engine hands
you refs in everything it returns, and takes them in every call that works on a page or an
annotation. You never build one; pass back the one you were given.

```ts
const { annotations } = await page.annotations.list();
await page.annotations.update(annotations[0].ref, { contents: 'Seen' });
```

This page is about what's inside a ref, for when you store one or compare two.

## Page refs

A page ref is the page's number inside the PDF file (its object number), not its position:

```ts
page.ref; // { kind: 'objectNumber', objectNumber: 12 }
```

That's why a page ref stays valid when pages move: after you move page 3 to the front, its ref
is the same, only its `index` in `doc.pages.list()` changes. A deleted page's number is never
given to another page.

When you already have a page's number (from your own database, say), `toPageRef(12)` makes the
ref. To compare two, use `pageRefsEqual(a, b)`.

## Annotation refs

An annotation ref also says which page the annotation is on. It comes in two kinds, depending on
how the annotation is stored in the file:

| Kind           | Looks like                                         |
| :------------- | :------------------------------------------------- |
| `objectNumber` | `{ kind: 'objectNumber', page, objectNumber: 48 }` |
| `baseIndex`    | `{ kind: 'baseIndex', page, baseIndex: 2 }`        |

Annotations the engine creates always get an object number. PDF files from other tools don't
always give their annotations one: an annotation stored without a number is named by the
position it has on its page in the file you uploaded, its `baseIndex`.

Both kinds name the annotation for life. Adding, moving or deleting other annotations never
changes a ref, and neither does editing the annotation itself: an annotation the file stores
without a number keeps its `baseIndex` even after the engine gives it a number of its own to
save a change. A deleted annotation's ref is never given to another annotation.

An annotation's `nm` (its name in the PDF) is data you can read and set, like its contents. It's
not how the engine finds the annotation. An annotation the engine creates without one gets a fresh
UUIDv7 name, and a copy made by an import with `attribution: 'stamp'` gets a new one.

To use a ref as a key in a `Map` or an object, turn it into a string with `annotationKey(ref)`.

## Naming an object before it exists

A ref usually comes back from the call that creates the thing. When you need it sooner (to show the
new annotation, select it or annotate a new page at once), take an object number from the
document's pool and create the object at it:

```ts
const objectNumber = doc.objectNumbers.take(); // null when the pool is empty: await reserve(n)
const pending = page.annotations.create({ subtype: 'square', box }, { objectNumber });

const ref = { kind: 'objectNumber', page: page.ref, objectNumber } as const;
await page.annotations.update(ref, { color: '#e11d48' }); // fine before the create answers
```

The object is made at exactly that number, so the ref is final from the start. Writes apply in the
order you make them, so the update above runs after the create. Pages and form fields work the
same way: `pages.insertBlank(spec, position, { objectNumbers })` names each new page, and
`forms.create(draft, { objectNumber, widgetObjectNumbers })` names a field and its widgets.

The numbers are this open document's alone: every `open()` has its own, even a second open of the
same document. A create that names a number the `doc` doesn't hold (never taken from the pool, or
already used) is refused with `ObjectNumberUnavailable` and `reason: 'not-held'`, and one a call
uses twice with `reason: 'taken'`; nothing is written. Leave the number out and the engine picks
one, as before. `doc.objectNumbers.reserve(n)` waits until at least `n` are held, for a large
paste.

A number you hold stays yours until a create uses it, except in two cases, which
`doc.objectNumbers.onLost` reports:

```ts
doc.objectNumbers.onLost(({ numbers, reason }) => dropPending(numbers));
```

- **`'versioned'`:** a signature published a new version of the document, which numbers every
  object anew.
- **`'reclaimed'`:** on the cloud, the numbers belong to an editing session, which stays alive
  while the document's event stream is connected or you keep writing. Left idle for 15 minutes
  without the stream, it expires, and its numbers may go to someone else. The pool stops handing
  out numbers it can no longer vouch for, and gets fresh ones with the next write or `reserve`.

Drop anything you showed at a lost number; a create that names one is refused with `not-held`.

## Other refs

Form fields and attachments have refs too, handed out the same way:

| Thing      | Its ref          | From a name you hold              |
| :--------- | :--------------- | :-------------------------------- |
| Form field | `field.ref`      | `toFieldRef('billing.name')`      |
| Attachment | `attachment.ref` | `toAttachmentRef('figures.xlsx')` |

Use the name helpers only for a name you stored yourself; see
[Forms](/docs/engine/forms#find-a-field) and
[Attachments](/docs/engine/documents/attachments#names-and-refs).

## Storing refs

A ref is plain data: store it as JSON and pass it back later, even after the document was saved
and opened again. A ref to an annotation that no longer exists fails with `NotFound`.

## Good to know

- **Why not the current position?** Positions change whenever something is added, moved or
  deleted. A `baseIndex` is the position in the file you uploaded, which never changes, so it
  can't end up pointing at a different annotation.
- **Why not the name?** Names are optional in PDF, and other tools don't always keep them unique.
  The engine refuses to create an annotation whose `nm` is already used on its page, so names it
  writes stay unique, but it never relies on them to find an annotation.

---

### Content Engine Concepts Permissions (`docs/content/engine/concepts/permissions.mdx`)

---
title: Permissions
description: What a user may do with a document, how to check it before you show a button, and what happens when a call is refused.
---

import { Callout } from '@/components/docs/callout';

# Permissions

A document can be opened with less than full power: a reviewer who may comment but not download,
a signer who may only fill in the form, a public link that only shows the pages. Each thing a user
may do is a permission, a string like `doc.download`.

Check a permission before you show the control for it:

```ts
if (doc.security.allows('doc.download')) {
  showDownloadButton();
}
```

The engine checks every call too. A call the user may not make fails with `Forbidden`, so hiding
a button is about honest UI, not safety.

## Where permissions come from

<Engine local>

You give them when you open the document:

```ts
const doc = await engine.open(
  { kind: 'bytes', bytes },
  { scope: ['doc.open', 'doc.render', 'doc.text.select', 'doc.annotate.read'] },
);
```

Without `scope`, the user gets every permission, and the engine logs a reminder once. That's
handy while you build; pass the permissions your users will really have, so you see the interface
they'll see.

A scope without `doc.open` can't open the document: `open()` fails with `Forbidden`, as it does
on a cloud server.

</Engine>

<Engine cloud>

They're in the user's token, which your backend creates. The server checks them on every request.
See [Authentication](/docs/engine/authentication#what-a-token-carries).

</Engine>

To list what the user was given (for a "you can…" panel), read `doc.security.scope`.

## Every permission

| Permission               | What it allows                                                                                             |
| :----------------------- | :--------------------------------------------------------------------------------------------------------- |
| `doc.open`               | Open the document and list its pages.                                                                      |
| `doc.render`             | Render pages as images.                                                                                    |
| `doc.text.select`        | Where text is: selecting it and the text layout.                                                           |
| `doc.text.search`        | Search, with where each match is.                                                                          |
| `doc.text.copy`          | The text itself: reading it, copying it, search snippets.                                                  |
| `doc.download`           | Download the file, attached files and stamp drawings, signed revisions; export annotations; extract pages. |
| `doc.print`              | Print, in the viewer.                                                                                      |
| `doc.pages.assemble`     | Rotate, move, delete, insert and name pages.                                                               |
| `doc.pages.modify`       | Change page content: flatten, redact (with the permissions below).                                         |
| `doc.annotate.read`      | See annotations.                                                                                           |
| `doc.annotate.modify`    | Create, change and delete any annotation.                                                                  |
| `doc.annotate.import`    | Import annotations with their original authors and dates.                                                  |
| `doc.forms.read`         | See form fields and their values.                                                                          |
| `doc.forms.fill`         | Fill in form fields.                                                                                       |
| `doc.forms.modify`       | Create, change and delete form fields.                                                                     |
| `doc.forms.import`       | Import form fields and values with who created and filled them in, and when.                               |
| `doc.forms.script`       | Write scripts, submit buttons and links into form fields.                                                  |
| `doc.sign`               | Sign the document.                                                                                         |
| `doc.sign.certify`       | Sign as the certifying signature.                                                                          |
| `doc.metadata.modify`    | Change the title, author and other metadata.                                                               |
| `doc.outline.modify`     | Add, change, move and delete outline items (bookmarks).                                                    |
| `doc.attachments.modify` | Add and remove attached files.                                                                             |
| `doc.redact`             | Apply redactions.                                                                                          |
| `pdf.permissions`        | What the PDF file itself allows (its permission flags); see [below](#the-pdfs-own-restrictions).           |
| `*`                      | Everything.                                                                                                |

Some permissions include others: `doc.annotate.modify` includes `doc.annotate.read`,
`doc.forms.fill` includes `doc.forms.read`, and `doc.forms.modify` includes `doc.forms.fill` and
`doc.forms.read`. Each page of these docs says which permission its calls
need.

`doc.annotate.import`, `doc.forms.import` and `doc.forms.script` come only from a scope that names
them (or `*`): no other permission, and not `pdf.permissions`, includes them. `doc.forms.script`
is about writing code into the form, never running it. Scripts run in each viewer, for everyone
who fills the form in, so putting one in is a separate decision from designing fields.

## Annotations

In a document many people work on, "may edit annotations" usually means "may edit _my_
annotations". Permissions for annotations can say exactly that, with the action and whose
annotations it covers:

```ts
const scope = [
  'doc.open',
  'doc.render',
  'doc.annotate.read',
  'annotations:create:self',
  'annotations:update:self',
  'annotations:delete:self',
];
```

| Part   | Values                                                                          |
| :----- | :------------------------------------------------------------------------------ |
| action | `create`, `update`, `delete`, `set-group`, or `*` for all four                  |
| whose  | `self` (the user's own), `all`, `group=<id>` (a group's), `createdBy=<user id>` |

A user's own annotations are the ones with their user id (`userId`). An `annotations:<action>:…`
permission replaces `doc.annotate.modify` for that action.
`['doc.annotate.modify', 'annotations:update:self']` lets a user create and delete any
annotation, but update only their own.

To check a single annotation before showing its edit controls:

```ts
doc.security.allowsAnnotation('create'); // may the user add annotations?
doc.security.allowsAnnotation('update', annotation); // may they change this one?
doc.security.allowsAnnotation('delete', annotation); // …delete this one?
```

A form field's widget is an annotation in the PDF, but changing it is designing the form: moving,
resizing, restyling or deleting a widget takes `doc.forms.modify`, and the `annotations:…`
permissions never apply to it. `allowsAnnotation('update', widget)` answers the same way.

## Form fields

When several people fill in one form, each fills in and signs their own fields: the buyer the
buyer's, the seller the seller's. A field belongs to a group (`groupId`, see
[Signers and groups](/docs/engine/forms/groups)), and permissions for fields name the action and
the group:

```ts
// The buyer: fills in and signs the buyer's fields, and nothing else.
const scope = ['doc.open', 'doc.render', 'fields:fill:group=buyer', 'fields:sign:group=buyer'];
```

| Part   | Values                                            |
| :----- | :------------------------------------------------ |
| action | `fill`, `sign`, `set-group`, or `*` for all three |
| which  | `all`, or `group=<id>` (the fields of one group)  |

A `fields:fill:…` permission replaces `doc.forms.fill` for filling in, and `fields:sign:…`
replaces `doc.sign` for signing, so `['doc.forms.fill', 'fields:fill:group=buyer']` fills in the
buyer's fields only. A field in no group is then left to users without a `fields:fill` permission.
Any `fields:` permission lets the user read the form: groups limit what they change, never what
they see.

A field a script calculates, such as a total, is filled in together with the field it's
calculated from. Writing it is allowed when the same change also fills in one of the user's own
fields, so the result travels with its input.

Designing the form (`doc.forms.modify`) isn't narrowed by any `fields:` permission. A signer's
token lists the permissions it needs and leaves out `doc.forms.modify` and `pdf.permissions`.

```ts
doc.security.allowsField('fill', field); // may the user fill this field in?
doc.security.allowsField('sign', field); // …sign it?
doc.security.allowsField('set-group', { groupId: 'buyer' }); // …put fields in this group?
```

## The PDF's own restrictions

A PDF can restrict itself: a file protected with an owner password may forbid printing or
copying. Add `pdf.permissions` to the scope to follow those restrictions. Without it, the engine
ignores them and only your permissions count.

<Engine local>

```ts
await engine.open(input, { scope: ['pdf.permissions'] });
```

`pdf.permissions` also lets you open and render the document, so this scope works on its own.

</Engine>

<Engine cloud>

Put `pdf.permissions` in the token's scope, next to the other permissions.

</Engine>

`pdf.permissions` gives what Acrobat allows on the same file. The names are the ones in Acrobat's
security summary for the file:

| The file allows                      | `pdf.permissions` gives                                                                                                                  |
| :----------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
| Printing                             | `doc.print`, and `doc.print.high` when it allows printing at full quality                                                                |
| Content copying                      | `doc.text.copy` and `doc.content.copy`, and with them a search's snippets                                                                |
| Changing the document                | `doc.pages.modify`, `doc.pages.assemble`, `doc.redact`, `doc.metadata.modify`, `doc.attachments.modify`, `doc.outline.modify`, `doc.forms.fill` and `doc.sign` |
| Commenting                           | `doc.annotate.modify`, `doc.forms.fill` and `doc.sign`                                                                                   |
| Changing the document and commenting | all of those, and `doc.forms.modify`                                                                                                     |
| Filling in form fields               | `doc.forms.fill` and `doc.sign`                                                                                                          |
| Document assembly                    | `doc.pages.assemble` and `doc.outline.modify`                                                                                            |

It always gives `doc.open`, `doc.render`, `doc.annotate.read`, `doc.forms.read`, `doc.text.select`
and `doc.text.search` too: the file's restrictions are about changing it and taking its content
out, never about seeing its annotations and fields, or finding and selecting its text, which
Acrobat allows in a file that forbids copying. Signing a
signature field counts as filling it in, so whatever allows filling in allows signing. Designing
the form takes both changing the document and commenting, as in Acrobat, where a file that only
allows commenting and filling in offers no form tools.

When the user unlocks the file with its owner password, the file's restrictions no longer apply.
See [Opening documents](/docs/engine/documents/opening#password-protected-files).

## What signatures forbid

A signed document can forbid changes itself: a certification says which changes may follow. That
binds every user, whatever their permissions. `allows()` answers no, and the call fails with
`ProtectedDocument` rather than `Forbidden`:

```ts
// On a document certified at level 2: filling in the form, but no new comments.
doc.security.allows('doc.forms.fill'); // true
doc.security.allows('doc.annotate.modify'); // false
```

See [Certify a document](/docs/engine/forms/signatures#certify-a-document) for what each level
allows.

## When a call is refused

A refused call fails with an `EngineError` whose code is `Forbidden`. It names the permission that
was missing:

```ts
import { EngineError, EngineErrorCode } from '@embedpdf/engine';

try {
  await doc.download();
} catch (error) {
  if (EngineError.is(error, EngineErrorCode.Forbidden)) {
    console.log(`missing ${error.details.required}`); // "doc.download"
  }
}
```

<Engine local>

<CloudPdfCallout
  title="Permissions that protect the file"
  href="https://www.cloudpdf.com/docs/engine/concepts/permissions"
  cta="How CloudPDF enforces permissions"
>

The local engine checks permissions in the user's browser, and the browser already has the whole
file. That keeps your interface and your code honest, but a user who takes the file out of the
page gets all of it.

With CloudPDF the file stays on your server and every call is checked there. A user without
`doc.download` never receives the file; a user without `doc.text.copy` never receives
the text; a reviewer with `annotations:update:self` can't change anyone else's comments, whatever
they send. Your code doesn't change.

</CloudPdfCallout>

</Engine>

## Good to know

### Building interface from permissions

The headless components turn permissions into one question per button, such as
`annotation.canEdit(ref)` and `selection.canCopy()`, so your interface never offers something that
will fail. See [Permissions](/docs/headless/concepts/permissions) in the headless docs.

### Groups

An annotation or a form field can belong to a group (`groupId`): a team or a department for
annotations, whoever fills them in for fields. What a user creates goes in their own group
(`identity.groupId`). `annotations:set-group:group=<id>` and `fields:set-group:group=<id>` let a
user put annotations or fields in that group; `annotations:update:group=<id>` lets them edit that
group's annotations, and `fields:fill:group=<id>` fill in that group's fields.

A `group=<id>` permission names the group it covers: the user doesn't need to be in it. To give
someone several groups, give them one permission per group.

### Importing with original authors

`doc.annotate.import` lets an [import](/docs/engine/annotations/import-export) write annotations
with the authors, dates and owners they had in the source document. Only give it to trusted jobs,
such as syncing from your own database: it lets the caller create annotations in someone else's
name.

`doc.forms.import` does the same for [form fields](/docs/engine/forms/import-export): an import
keeps who created each field and who filled it in, instead of naming the importing user. On a
signed form, who filled a field in is evidence, so give it as carefully.

### Scripts in forms

`doc.forms.script` decides who may put code into a form: scripts that check or calculate a field,
and buttons that submit the form or open a link. A script runs for everyone who fills the form in
afterwards. In your viewer it can change what they type or hide fields; opened in another PDF app,
a script or a submit button can send their answers anywhere. So a user who may design fields
(`doc.forms.modify`) doesn't get it with that. Buttons that only go to a page, reset the form, or
show and hide fields need `doc.forms.modify` alone. An import without it leaves the scripts, submits
and links out and copies the rest.

It doesn't make a script's result trustworthy: a client can skip scripts and send any value. A
server that must trust a calculated value calculates it itself.

---

### Content Engine Documents Attachments (`docs/content/engine/documents/attachments.mdx`)

---
title: Attachments
description: List, download, add and delete the files attached to a document.
---

# Attachments

A PDF can carry other files inside it: a spreadsheet with the numbers behind a report, the
original of a scanned contract. PDF apps list them in an attachments panel.

```ts
const { attachments } = await doc.attachments.list();

for (const attachment of attachments) {
  console.log(attachment.name, attachment.size);
}

const bytes = await doc.attachments.download(attachments[0].ref);
```

## List

`list()` returns the attached files, sorted by name. It doesn't read the files themselves, so it's
quick however large they are.

| Field         | What it is                                                         |
| :------------ | :----------------------------------------------------------------- |
| `ref`         | The attachment's address. Pass it to `download()` and `delete()`.  |
| `name`        | The file name, such as `'figures.xlsx'`.                           |
| `mimeType`    | The file type, such as `'application/pdf'`, or `null`.             |
| `description` | A description, or `null`.                                          |
| `size`        | The size in bytes, or `null`.                                      |
| `checksum`    | The file's MD5 checksum, as lowercase hex, or `null`.              |
| `createdAt`   | When the file was created, as an ISO date, or `null`.              |
| `modifiedAt`  | When the file was last changed, as an ISO date, or `null`.         |
| `index`       | Its position in the list. Changes when files are added or deleted. |

A field reads `null` when the PDF doesn't say.

## Download one

`download(ref)` returns the file's bytes. Its name and type are the ones the list gave you, so
handing it to the user takes both:

```ts
const bytes = await doc.attachments.download(attachment.ref);

const url = URL.createObjectURL(new Blob([bytes], { type: attachment.mimeType ?? '' }));
const link = document.createElement('a');
link.href = url;
link.download = attachment.name;
link.click();
URL.revokeObjectURL(url);
```

A [file attachment annotation's](/docs/engine/annotations/file-attachments) file downloads the
same way: `page.annotations.downloadResource(ref, 'file')` returns its bytes.

## Attach a file

`create()` takes the bytes and a name, and returns the new attachment:

```ts
const { attachment } = await doc.attachments.create({
  data: bytes,
  name: 'figures.xlsx',
  mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  description: 'Numbers for the Q2 report',
});
```

`data` is a `Uint8Array`, an `ArrayBuffer` or a `Blob`. A `File` the user picked brings its own
name and type, so it's enough on its own:

```ts
const file = input.files![0];
await doc.attachments.create({ data: file });
```

Only `data` and a name are needed; a `File` brings its name, and a `File` or `Blob` brings its
type. The engine never guesses a type: without one, `mimeType` reads `null`. It records the size,
the checksum and the time you attached it.

Names are unique in a document: attaching a second file with a name that's already there fails
with `InvalidArg`. Rename it and try again.

## Delete

```ts
await doc.attachments.delete(attachment.ref);
```

The attachment disappears from the list. Its bytes stay in earlier versions of the file until you
save with `mode: 'rewrite'`; see [Good to know](#deleted-files-and-saving).

## Undo

`create()` and `delete()` are changes: each returns its `opId` in `meta`, and
`doc.apply({ undoOf: opId })` undoes it. An undo of a delete brings the same file back, with its
bytes, description and dates, and fires `attachments.restored`. To add or remove several files as
one step, apply their ops as one change. See [Changes & undo](/docs/engine/documents/changes).

```ts
const { meta } = await doc.attachments.delete(attachment.ref);
await doc.apply({ undoOf: meta.opId }); // the file is back
```

An undo leaves alone what someone changed since: the undo of an attach doesn't delete a file
someone else attached under that name since, even with the same bytes, and the undo of a delete
doesn't bring a file back while another file has its name.

## Attachments or file-attachment annotations

A PDF can hold a file in two places:

- **An attachment** (this page) belongs to the whole document. PDF apps list it in their
  attachments panel.
- **A [file attachment annotation](/docs/engine/annotations/file-attachments)** is pinned to a spot
  on a page, shown as an icon, and can carry a comment.

They're separate: `doc.attachments.list()` doesn't include files pinned to pages, and deleting an
attachment never touches an annotation.

## Permissions

| Call                   | Needs                                        |
| :--------------------- | :------------------------------------------- |
| `list()`               | `doc.open`                                   |
| `download()`           | `doc.download`: the file leaves the document |
| `create()`, `delete()` | `doc.attachments.modify`                     |

So a user can see which files a document carries without being able to take them out.

## Good to know

### Deleted files and saving

Deleting an attachment removes it from the document as it is now. An incremental save still
contains the version you opened, and with it the deleted file. When a file must really be gone,
save with `mode: 'rewrite'`. See [Saving](/docs/engine/documents/saving#incremental-or-rewrite).

### Names and refs

An attachment's ref is its name in the PDF's list of files, which is why names are unique. When
you stored a name yourself, `toAttachmentRef('figures.xlsx')` turns it back into a ref. A file
another app attached may show a different `name` than the one in its ref; always pass the ref.

Adding or deleting a file changes the `index` of the files after it in name order, never their
refs.

### Large files

<Engine local>

The browser holds a file it downloads in memory, so the local engine refuses a file that decodes
to more than `ATTACHMENT_LIMITS.decodedBytes` (256 MB) with `PayloadTooLarge`. That goes for
attachments and file attachment annotations alike; `details.limit` names the limit. A file's own size can't be trusted for this: a small compressed
file can decode to gigabytes.

</Engine>

<Engine cloud>

The server decodes the file and applies its own limit, refusing a larger file with
`PayloadTooLarge`.

</Engine>

### Events

Other code can follow changes with the `attachments.created`, `attachments.deleted` and
`attachments.restored` events. See [Events](/docs/engine/concepts/events).

---

### Content Engine Documents Changes (`docs/content/engine/documents/changes.mdx`)

---
title: Changes & undo
description: Apply several writes as one user action, all or none, and undo or redo any change.
---

import { Callout } from '@/components/docs/callout';

# Changes & undo

A change is one user action: the writes it makes, applied together, all or none. Every change can
be undone, and so can every annotation, form or metadata call you make on its own.

```ts
import { toFieldRef } from '@embedpdf/engine';

const approved = await doc.apply({
  ops: [
    { type: 'annotations.update', ref: square.ref, patch: { color: '#16a34a' } },
    { type: 'forms.setValue', field: toFieldRef('status'), value: { value: 'Approved' } },
  ],
});

// Both writes go back:
await doc.apply({ undoOf: approved.meta.opId });
```

## Apply a change

`apply()` takes a list of `ops`. Each op is one of the calls you already know, written as data: a
`type`, and the arguments the call takes, by name. The engine applies them in order:

```ts
const result = await doc.apply({
  ops: [
    {
      type: 'annotations.create',
      page: page.ref,
      data: { subtype: 'square', box: { x: 72, y: 92, width: 200, height: 100 } },
    },
    { type: 'annotations.delete', ref: oldSquare.ref },
    { type: 'metadata.update', patch: { modifiedAt: new Date() } },
  ],
});
```

When one op fails, none of them is written, no event fires, and `apply()` rejects with that op's
error. The document is as it was before the call.

| Op `type`                      | Its fields                                                | The call it stands for                                                          |
| :----------------------------- | :-------------------------------------------------------- | :------------------------------------------------------------------------------ |
| `annotations.create`           | `page`, `data`, `objectNumber`, `resources`               | `page.annotations.create(data, { objectNumber, resources })`                    |
| `annotations.update`           | `ref`, `patch`, `resources`, `expect`                     | `page.annotations.update(ref, patch, { resources })`                            |
| `annotations.delete`           | `ref`, `expect`                                           | `page.annotations.delete(ref)`                                                  |
| `annotations.reorder`          | `page`, `refs`, `position`                                | `page.annotations.reorder(refs, position)`                                      |
| `forms.setValue`               | `field`, `value`, `expect`                                | `doc.forms.setValue(field, value)`                                              |
| `forms.reset`                  | `fields`                                                  | `doc.forms.reset(fields)`                                                       |
| `forms.create`                 | `draft`, `objectNumber`, `widgetObjectNumbers`            | `doc.forms.create(draft, { objectNumber, widgetObjectNumbers })`                |
| `forms.update`                 | `field`, `patch`, `expect`                                | `doc.forms.update(field, patch)`                                                |
| `forms.delete`                 | `field`, `expect`                                         | `doc.forms.delete(field)`                                                       |
| `forms.addWidget`              | `field`, `placement`, `objectNumber`, `splitObjectNumber` | `doc.forms.addWidget(field, placement, { objectNumber, splitObjectNumber })`    |
| `forms.removeWidget`           | `field`, `widget`                                         | `doc.forms.removeWidget(field, widget)`                                         |
| `forms.deleteWidget`           | `widget`, `expect`                                        | `doc.forms.deleteWidget(widget)`                                                |
| `forms.updateWidget`           | `widget`, `patch`, `expect`                               | `doc.forms.updateWidget(widget, patch)`                                         |
| `forms.reorderWidgets`         | `page`, `widgets`, `position`                             | `doc.forms.reorderWidgets(widgets, position)`                                   |
| `forms.reorderCalculations`    | `fields`, `position`                                      | `doc.forms.reorderCalculations(fields, position)`                               |
| `forms.setSignatureAppearance` | `field`, `appearance`                                     | `doc.forms.setSignatureAppearance(field, appearance)`                           |
| `forms.setDisplay`             | `field`, `display`                                        | Shows or hides a field's widgets, as a form script does                         |
| `forms.setAppearanceText`      | `field`, `text`                                           | Sets the text a field shows without changing its value, as a format script does |
| `metadata.update`              | `patch`, `expect`                                         | `doc.metadata.update(patch)`                                                    |
| `metadata.updateCustom`        | `patch`                                                   | `doc.metadata.custom.update(patch)`                                             |
| `outline.create`               | `parent`, `position`, `data`, `objectNumber`              | `doc.outline.create(data, { parent, position, objectNumber })`                  |
| `outline.update`               | `ref`, `patch`, `expect`                                  | `doc.outline.update(ref, patch)`                                                |
| `outline.move`                 | `refs`, `parent`, `position`                              | `doc.outline.move(refs, parent, position)`                                      |
| `outline.delete`               | `ref`, `expect`                                           | `doc.outline.delete(ref)`                                                       |
| `pages.rotate`                 | `pages`, `rotation`                                       | `doc.pages.rotate(pages, rotation)`                                             |
| `pages.reorder`                | `pages`, `position`                                       | `doc.pages.reorder(pages, position)`                                            |
| `pages.delete`                 | `pages`                                                   | `doc.pages.delete(pages)`                                                       |
| `pages.insertBlank`            | `spec`, `position`, `objectNumbers`                       | `doc.pages.insertBlank(spec, position, { objectNumbers })`                      |
| `pages.insert`                 | `source`, `position`                                      | `doc.pages.insert(source, position)`                                            |
| `attachments.create`           | `file`, `resources`                                       | `doc.attachments.create({ data, ...file })`                                     |
| `attachments.delete`           | `ref`                                                     | `doc.attachments.delete(ref)`                                                   |

Put everything one action does into one change: it's one step in the user's undo history, and
its events arrive together.

### What comes back

`result.items` has one item per op, in op order. Each item is what the op's call returns, with
the op's `type` added (and the `page`, for annotation ops). `result.meta` is about the whole
change:

```ts
const created = result.items[0];
if (created.type === 'annotations.create') select(created.annotation.ref);

result.meta.opId; // the change's name: what undo takes
result.meta.undoable; // true
```

| Field                | What it is                                                                                |
| :------------------- | :---------------------------------------------------------------------------------------- |
| `meta.opId`          | The change's `opId`: the one you passed, or one made up for it.                           |
| `meta.undoable`      | Whether `{ undoOf: opId }` can undo it. See [What can't be undone](#what-cant-be-undone). |
| `meta.affectedPages` | Every page whose annotations the change touched.                                          |

A change with no ops writes nothing: it resolves at once, with no items, and `undoable: false`.

### Refer to something an earlier op makes

A later op can work on what an earlier op of the same change creates. Take the new object's
number from the document's pool first, so its ref is known before anything is written:

```ts
await doc.objectNumbers.reserve(1);
const objectNumber = doc.objectNumbers.take()!;
const note = { kind: 'objectNumber', page: page.ref, objectNumber } as const;
const rect = { x: 500, y: 72, width: 20, height: 20 };

await doc.apply({
  ops: [
    {
      type: 'annotations.create',
      page: page.ref,
      data: { subtype: 'text', rect, contents: 'Is this total right?' },
      objectNumber,
    },
    {
      type: 'annotations.create',
      page: page.ref,
      data: { subtype: 'text', rect, contents: 'Asking finance.', reply: { to: note } },
    },
  ],
});
```

Undo this change and the note and its reply go together. Object numbers are explained in
[Refs & identity](/docs/engine/concepts/identity#naming-an-object-before-it-exists).

## Undo and redo

Undo a change by its `opId`:

```ts
const undo = await doc.apply({ undoOf: approved.meta.opId });
```

That works for every change, and for every call the table above lists, made on its own: each
returns its `opId` in `meta`, the way a change does.

```ts
const { meta } = await page.annotations.update(ref, { contents: 'Seen' });
await doc.apply({ undoOf: meta.opId });
```

An undo is a change of its own, with its own `opId`. Undo the undo, and the change applies again:
that's redo.

```ts
await doc.apply({ undoOf: undo.meta.opId }); // redo
```

An undo button needs two lists of `opId`s:

```ts
import type { Change } from '@embedpdf/engine';

const done: string[] = [];
const undone: string[] = [];

async function act(change: Change) {
  const { meta } = await doc.apply(change);
  done.push(meta.opId);
  undone.length = 0;
}

async function undo() {
  const opId = done.pop();
  if (opId) undone.push((await doc.apply({ undoOf: opId })).meta.opId);
}

async function redo() {
  const opId = undone.pop();
  if (opId) done.push((await doc.apply({ undoOf: opId })).meta.opId);
}
```

Undo works through the engine, not on a copy you keep: it reads the document as it is now, and
puts back what the change wrote.

### When the document changed since

An undo puts back only what still shows what the change set. Someone may have changed the same
thing since: another user on the cloud, or your own later change. What they changed is left as
they left it:

- A field someone changed since keeps its new value. The item names it in `skipped`, such as
  `skipped: ['color']`.
- An op whose annotation or field is gone, or was changed all over, is left alone entirely. Its
  item is `{ type: 'skipped', op }`, with `op` the type of item it would have been.
- A create isn't undone when someone changed what it made, or replied to it.
- A reorder puts each annotation or widget back next to the neighbours it had, unless it was
  moved since or the one below it changed. Neighbours that are gone are skipped over.
- A page move puts each page back after the page it followed, unless it was moved since or that
  page is gone. A page rotate turns back only the pages that still have the rotation it set.
- A page insert isn't undone when someone annotated or turned the new page since, or when it
  would leave the document without pages.
- An attached file isn't removed again when another file took its name since, and a deleted one
  doesn't come back while another file has its name.

Nothing is written for what's left alone, and no event fires for it. Tell the user when it
happened:

```ts
const undo = await doc.apply({ undoOf: opId });
const leftAlone = undo.items.some((item) => item.type === 'skipped' || 'skipped' in item);
if (leftAlone) showToast('Some of this was changed since, and was kept.');
```

It goes by value: a field changed and then set back to what your change set counts as unchanged,
and is undone. Undoing the same change twice is harmless: the second undo finds nothing left to
put back.

Undoing a delete brings back what was deleted as it was: the same refs, its place among the
page's annotations, its replies and popup, and who wrote it. The item is `annotations.restore`,
with the annotations that came back, `forms.restore`, with the field, `forms.restoreWidget`, with
the widget back at its place and in its field, `pages.restore`, with the pages back after the
page each followed (first when that page is gone), with their annotations and their names, or
`attachments.restore`, with the file back under its name.

### What can't be undone

`meta.undoable` says whether a write can be undone. It's `false` for:

- **A write that changed nothing**, such as a change with no ops, a reset of fields already at
  their defaults, or hiding a field that's already hidden.
- **Writes a change can't hold**: page edits, form data imports. They're writes of
  their own. An [annotation import](/docs/engine/annotations/import-export#undo-an-import) is a
  change and can be undone.
- **Final writes**: [applying redactions](/docs/engine/documents/redaction),
  [flattening](/docs/engine/documents/pages#flatten-a-pages-annotations),
  [signing](/docs/engine/forms/signatures) and
  [repairing a form](/docs/engine/forms/authoring#repair-a-broken-form). They take content out of
  the document or seal it, so there's nothing to go back to.

A final write also ends undo for every change before it. Undoing one of those fails with
`UndoUnavailable` and `details.reason: 'final-change'`. Changes after the final write can be
undone as usual.

```ts
import { EngineError, EngineErrorCode } from '@embedpdf/engine';

try {
  await doc.apply({ undoOf: opId });
} catch (error) {
  if (EngineError.is(error, EngineErrorCode.UndoUnavailable)) {
    showToast('This can no longer be undone.');
  } else {
    throw error;
  }
}
```

## Guard a change with `expect`

By default, a change writes over whatever is there: the last write wins. When your change only
makes sense if the document still holds what the user saw, say so with `expect`. It takes the
same shape as what the op writes:

```ts
await doc.apply({
  ops: [
    {
      type: 'annotations.update',
      ref: square.ref,
      patch: { color: '#16a34a' },
      expect: { color: '#e11d48' }, // only while it's still red
    },
  ],
});
```

When the document doesn't match, the whole change is refused with `ChangeConflict`, and nothing
is written. `error.details.opIndex` is the op that didn't match and `error.details.fields` the
fields that differed. `annotations.delete` and `forms.delete` expect the values the thing has
before it goes. A reorder takes no `expect`: its position names a neighbour, which is already
what it assumes.

## Asking again

The `opId` names the change. Asked again with the same `opId`, a change isn't applied a second
time: you get the first answer back, and no event fires again. A change that was refused answers
its refusal again, even if it would now apply, so a retry can never land after something that came
later.

```ts
const opId = crypto.randomUUID();
const first = await doc.apply(change, { opId });
const again = await doc.apply(change, { opId }); // the same result; nothing written
```

A different change under an `opId` that already has an answer fails with `IdempotencyKeyReused`.
Use a fresh `opId` for every change. See [Events](/docs/engine/concepts/events#which-write-an-event-came-from)
for what an `opId` may contain.

## Who may undo

Only the user who made a change can undo it; anyone else gets `Forbidden`. An undo is also checked
against what the user may do now, like any write: a user who can no longer change annotations
can't undo an annotation change either. A change needs, op by op, the permissions each call
needs; see [Permissions](/docs/engine/concepts/permissions).

<Engine local>

## How long undo lasts

The engine keeps what undo needs while the document is open, in this tab: the last 4,096 changes,
and up to 64 MB of the objects they replaced. When it has to let go of the oldest, undoing them
fails with `UndoUnavailable` and `details.reason: 'expired'`. Closing the document ends its undo
history.

<CloudPdfCallout
  title="Undo that outlasts the tab"
  href="https://www.cloudpdf.com/docs/engine/documents/changes"
  cta="Read about changes in CloudPDF"
>

With CloudPDF, the server keeps each change and what undoes it for 30 days. A user can undo after
a reload, or from another device, and the undo leaves alone what colleagues changed since.

</CloudPdfCallout>

</Engine>

<Engine cloud>

## How long undo lasts

The server keeps each change, and what undoes it, for 30 days (your deployment can set another
period). A user can undo a change after a reload or from another device. After that, undoing it
fails with `UndoUnavailable` and `details.reason: 'expired'`.

## How changes are sent

Changes go to the server one request at a time per document, in the order you make them. Changes
you make while a request is on its way wait together, and go out as one request when it returns:
a burst of edits on a slow connection becomes a few requests, not one per edit. Each `apply()`
still resolves with its own result, and a change and its undo can travel in the same request.

A request lost on the network goes again with the same `opId`s, so nothing is applied twice.

A change holds at most 512 ops; more fails with `PayloadTooLarge`. Split a bigger job into
several changes.

</Engine>

## Good to know

### Events

A change fires the events its ops' calls fire, in op order, all with the change's `opId` in
`origin.tx`. An undo fires what it did: `annotations.updated` for a field put back,
`annotations.deleted` for a create taken back, `annotations.restored`, `forms.restored`,
`outline.restored`, `pages.restored` and `attachments.restored` for what came back. Its events carry `origin.undoOf`, the `opId` of the change it undid. See
[Events](/docs/engine/concepts/events).

### How the engine puts things back

When a change runs, the engine keeps a copy of everything it's about to replace or delete. When
nothing touched those objects since, an undo puts the copies back exactly, down to the
annotation's appearance. When something did, it falls back to putting back each field that still
holds the value the change wrote.

<Engine cloud>

### From your server

The server takes changes over HTTP too, so a backend job can make them and undo them with the
same `opId`s. See [Apply changes](/docs/api-reference/document-operations/changes) in the API
reference.

</Engine>

---

### Content Engine Documents Fonts (`docs/content/engine/documents/fonts.mdx`)

---
title: Fonts
description: Give the engine your own fonts, for text in any script or your brand typeface, and fill in characters a document's fonts don't have.
engines: [local]
---

import { Callout } from '@/components/docs/callout';

# Fonts

Text the engine draws (a free text annotation, a filled-in form field, a redaction label) needs a
font that has its characters. The 14 standard PDF fonts only cover Latin text. To write Chinese,
Cyrillic, Arabic or your brand typeface, register a font with the engine and use it by a key you
choose:

```ts
const response = await fetch('/fonts/NotoSansSC-Regular.otf');

await engine.fonts.register({
  key: 'noto-sc',
  familyName: 'Noto Sans SC',
  data: new Uint8Array(await response.arrayBuffer()),
});
```

Registered fonts belong to the engine, not to one document: every document opened on the engine
can use them, until you clear them or destroy the engine.

## Register a font

`register()` takes the font file (TTF or OTF) and a `key`:

| Field        | What it is                                                   | Default            |
| :----------- | :----------------------------------------------------------- | :----------------- |
| `key`        | Your name for the font. You use it wherever you pick a font. | required           |
| `data`       | The font file's bytes, a `Uint8Array` or an `ArrayBuffer`.   | required           |
| `familyName` | The family the PDF names the font by.                        | read from the file |
| `weight`     | `100` to `900`.                                              | read from the file |
| `italic`     | Whether it's the italic style.                               | read from the file |

Registering a key a second time does nothing and returns the first font, so it's safe to call on
every page load. A file the engine can't read fails with `InvalidArg`. The 14 standard names
(`'helvetica'`, `'times-roman'`, `'courier'`, …) are taken: registering one of them fails with
`InvalidArg`.

To load fonts while the engine starts, before the first document opens, pass them to
`localEngine()` with a `url` or with `data`:

```ts
const engine = localEngine({
  fonts: [{ key: 'brand-sans', url: '/fonts/BrandSans-Regular.ttf' }],
  fallbackFonts: [{ key: 'noto-sc', url: '/fonts/NotoSansSC-Regular.otf' }],
});
```

`fonts` are only used when you name them; `fallbackFonts` also [fill in missing
characters](#automatic-fallback-for-missing-characters).

## Use a font on free text

A free text annotation's `fontFamily` takes a standard font name or a key you registered:

```ts
await page.annotations.create({
  subtype: 'free-text',
  box: { x: 60, y: 132, width: 300, height: 60 },
  contents: '这是一个测试',
  fontFamily: 'noto-sc',
});
```

A key you never registered fails with `InvalidArg`: the engine doesn't quietly swap in another
font.

When you save, the PDF carries only the characters the text uses, not the whole font. A CJK font
of several megabytes adds only what those characters need, and the text shows the same in every
PDF app.

In [rich text](/docs/engine/annotations/free-text), a run names its font by family, the way the PDF
does: `{ text: '世界', style: { family: 'Noto Sans SC' } }`. The engine finds a registered font by
its family or its key. A family it can't find anywhere is drawn in Helvetica, so the text still
shows.

When you read the annotation back, its `fontFamily` is your key if the font is registered on the
engine that reads it, and the font's family name otherwise. So a document you saved comes back
with the same key, as long as the font is registered.

## Show the font in the browser too

The engine puts the font into the PDF; the browser doesn't know it. The text editor and any
annotation the browser draws itself use the CSS font family `epdf-` and the font's key, such as
`epdf-noto-sc`, so give the browser the same file under that name. `mountWebFont()` names it for
you, and the `epdf-` keeps a font of the same name on your page from standing in for it:

```ts
import { mountWebFont } from '@embedpdf/web';

await engine.fonts.register({ key: 'noto-sc', familyName: 'Noto Sans SC', data });
const unmount = await mountWebFont('noto-sc', data);

// When you no longer need it:
unmount();
```

Mounting a key twice is fine: the font stays until the last `unmount()`. The viewer does both
steps for you from its `engine.fonts` option; see
[Fonts for text annotations](/docs/viewer/features/annotations#fonts).

## Document font settings

Two settings per document shape the text the engine draws from then on. They're not saved in the
file:

```ts
await doc.fonts.setEmbeddingPolicy('full');
await doc.fonts.setTypographicFeatures(true);
```

| Setting                  | What it does                                                                                                            | Default     |
| :----------------------- | :---------------------------------------------------------------------------------------------------------------------- | :---------- |
| `setEmbeddingPolicy`     | How much of a font goes into the PDF: `'subset'` (only the characters used), `'full'` (the whole font), or `'default'`. | `'default'` |
| `setTypographicFeatures` | Kerning and ligatures for Latin text in rich text.                                                                      | off         |

`'default'` embeds only the characters used for annotation text, and the whole font for form
fields. A font whose licence doesn't allow a partial copy is always embedded whole, and a font
already in the document is never added again.

Kerning and ligatures are off by default because Acrobat draws annotation text without them, and
the engine matches what Acrobat draws.

## Automatic fallback for missing characters

A registered font is only used where you name it. Add it to the fallback list, and the engine
also uses it for any character the chosen font doesn't have, in the text it draws and when it
renders pages:

```ts
await engine.fonts.register({ key: 'noto-sc', familyName: 'Noto Sans SC', data });
await engine.fonts.addFallback('noto-sc');

// "Hello " is drawn in Helvetica (the default), "世界" in Noto Sans SC.
await page.annotations.create({
  subtype: 'free-text',
  box: { x: 60, y: 192, width: 300, height: 60 },
  contents: 'Hello 世界',
});
```

Fallback fonts are tried in the order you add them; the first one that has the character wins.

## Manage fonts

```ts
engine.fonts.list(); // every registered font, in the order you registered them (no await: it's in memory)
await engine.fonts.clearFallbacks(); // empty the fallback list; the fonts stay registered
await engine.fonts.clear(); // unregister every font and empty the fallback list
```

<Callout type="info">
  The engine keeps every registered font in memory, and a CJK font is several megabytes. Register
  the few fonts you use rather than a whole library.
</Callout>

## Good to know

### Font licences

A font file says how it may be embedded, and the engine follows it. `register()` returns what it
found:

```ts
const font = await engine.fonts.register({ key: 'brand-serif', data });
font.embeddingPermission; // 'installable' | 'editable' | 'preview-and-print'
font.editingAuthorized; // may new text be written with it?
```

A font whose licence forbids embedding is refused when you register it. A `'preview-and-print'`
font shows text a document already has, but can't be used for new text: a free text annotation
that names it fails, and the fallback list skips it. When your licence does allow editing, say so:

```ts
await engine.fonts.authorizeEditing('brand-serif');
```

### Variable fonts

A variable font is fixed to one style when you register it: the `weight` you give, or the font's
default. The PDF then carries an ordinary font of that style, and the font's `instanced` field is
`true`.

### Several fonts at once

`registerAll([...])` registers a list of fonts in order and returns them in the same order.

---

### Content Engine Documents Metadata (`docs/content/engine/documents/metadata.mdx`)

---
title: Metadata
description: Read and change a document's title, author, subject, keywords, dates and your own keys.
---

# Metadata

A PDF carries information about itself: a title, an author, when it was made. PDF apps show it in
their document properties.

```ts
const metadata = await doc.metadata.get();
console.log(metadata.title, metadata.author);

await doc.metadata.update({ title: 'Q2 Proposal (final)' });
```

## Read

`get()` returns every standard field. A field the file doesn't set is `null`:

| Field        | What it is                                                  |
| :----------- | :---------------------------------------------------------- |
| `title`      | The document's title.                                       |
| `author`     | Who wrote it.                                               |
| `subject`    | What it's about.                                            |
| `keywords`   | Keywords, as one string (often separated by commas).        |
| `creator`    | The app the document was made in, such as a word processor. |
| `producer`   | The app that turned it into a PDF.                          |
| `createdAt`  | When it was created, as an ISO date.                        |
| `modifiedAt` | When it was last changed, as an ISO date.                   |
| `trapped`    | A print-shop setting: `'true'`, `'false'` or `'unknown'`.   |

Keys of your own, and those other apps added, are read separately: see
[Your own keys](#your-own-keys).

## Change

`update()` changes only the fields you pass. `null` removes a field:

```ts
const { metadata } = await doc.metadata.update({
  title: 'Q2 Proposal (final)',
  author: 'Dana Smith',
  subject: null, // remove the subject
});
```

It returns the metadata as it is after the change, the same shape `get()` returns. What you read
goes back in as it is, so "change a copy, then save it" works:

```ts
const current = await doc.metadata.get();
await doc.metadata.update({ ...current, keywords: `${current.keywords ?? ''}, approved` });
```

An empty string is a value, not a removal: `title: ''` sets an empty title.

## Dates

Dates are ISO strings, with the time zone offset the PDF gives:

```ts
metadata.createdAt; // '2017-07-12T21:44:38-07:00'
```

They're strings, not `Date` objects, so a date survives JSON and goes back into `update()` as it
is. `new Date(metadata.createdAt)` turns one into a `Date` when you need to compute with it.

When you write a date, pass an ISO string or a `Date`. A string keeps its offset; a `Date` is
written in UTC:

```ts
await doc.metadata.update({
  createdAt: '2026-01-04T09:00:00+01:00',
  modifiedAt: new Date(),
});
```

PDF dates have whole seconds, so milliseconds are dropped. A string that isn't an ISO date and
time fails with `InvalidArg`.

## Your own keys

Store your own values in the document: a contract number, who reviewed it. Other apps add keys
like these too, and PDF apps show them as custom properties. They have their own object,
`doc.metadata.custom`, with the same two calls:

```ts
const custom = await doc.metadata.custom.get();
// { reviewedBy: 'dana', draftOwner: 'sam' }, or {} when there are none
```

`update()` works like it does for the standard fields, with each key as a field: a string sets
the key, `null` removes it, and keys you leave out stay as they are.

```ts
const { custom } = await doc.metadata.custom.update({
  reviewedBy: 'lee',
  contractId: 'C-2026-114',
  draftOwner: null, // remove this key
});
```

It returns the keys as they are after the change, the same shape `get()` returns.

A key is 1 to 127 printable ASCII characters, doesn't start with `/`, and isn't the name of a
standard field (`Title`, `Author`, `CreationDate`, …): use `doc.metadata.update()` for those. An
invalid key fails with `InvalidArg`, which names the key, and nothing is written.

## Permissions

Reading metadata needs `doc.open`. Changing it, your own keys included, needs
`doc.metadata.modify`; without it, `update()` fails with `Forbidden`.

## Good to know

### The modified date

The engine doesn't change `modifiedAt` for you, neither when you update metadata nor when you
save. Set it yourself when your app wants it to say when the document last changed:

```ts
await doc.metadata.update({ modifiedAt: new Date() });
```

### Where it's stored

The fields are the PDF's document information (the `/Info` dictionary, with keys like `/Title`
and `/CreationDate`). Your own keys live in the same dictionary under their own names:
`reviewedBy` is `/reviewedBy`. `trapped` is `'unknown'` both when the file says so and when it
doesn't say.

### Why your own keys are separate

`update()` keeps what you leave out one level deep: each field you pass replaces that field.
Your own keys are fields too, so they get an object of their own, where each key is one of those
fields. Adding a key never touches the others, and a key can't be mistaken for a standard field.

Other code can follow changes with the `metadata.updated` and `metadata.customUpdated` events.
See [Events](/docs/engine/concepts/events).

---

### Content Engine Documents Opening (`docs/content/engine/documents/opening.mdx`)

---
title: Opening documents
description: Open a PDF, unlock a password-protected file, say who is working on it, and close it again.
---

# Opening documents

`engine.open()` gives you a handle to one document. Everything you do with the document starts
from that handle.

<Engine local>

```ts
const response = await fetch('/contract.pdf');
const doc = await engine.open({ kind: 'bytes', bytes: await response.arrayBuffer() });
```

</Engine>

<Engine cloud>

```ts
const doc = await engine.open({ kind: 'token', token: () => fetchDocumentToken('contract') });
```

</Engine>

## Open a document

<Engine local>

Give the engine the file's bytes, as an `ArrayBuffer` or a `Uint8Array`. Where they come from
doesn't matter. A file the user picked:

```ts
const file = input.files![0];
const doc = await engine.open({ kind: 'bytes', bytes: await file.arrayBuffer() });
```

A file on disk, in Node:

```ts
import { readFile } from 'node:fs/promises';

const doc = await engine.open({ kind: 'bytes', bytes: await readFile('contract.pdf') });
```

The engine gives each document an id (`doc.id`). To choose it yourself, pass `id` (any input
takes one); it must be unique among the documents open on this engine.

A file that isn't a PDF, or is too damaged to read, fails with `DocOpenFailed`.

</Engine>

<Engine cloud>

The document is already on your server, so you open it by reference. There are three ways,
depending on where the credential comes from:

| Input                           | When to use it                                                   |
| :------------------------------ | :--------------------------------------------------------------- |
| `{ kind: 'token', token }`      | Your backend created a token for this user and this document.    |
| `{ kind: 'id', id }`            | Your app has a token for your whole account (admin tools, jobs). |
| `{ kind: 'share', shareToken }` | A public share link from the CloudPDF dashboard.                 |

[Authentication](/docs/engine/authentication) shows how to create tokens and when to use each.

`open()` fetches the document's details from the server, never the file itself. Pages, text and
images arrive as you ask for them.

</Engine>

## Password-protected files

A PDF can have its own password. When you know it, pass it when you open:

```ts
const doc = await engine.open(input, { password: 'secret' });
```

When you don't, open without one and ask the user only if the file needs it.
`doc.security.passwordPrompt` tells you:

```ts
const doc = await engine.open(input);

if (doc.security.passwordPrompt.state === 'required') {
  const password = await askUserForPassword();
  await doc.security.unlock({ password });
}
```

| `passwordPrompt.state` | What it means                                                                      |
| :--------------------- | :--------------------------------------------------------------------------------- |
| `'none'`               | Nothing to do: the file has no password, or it's already fully unlocked.           |
| `'required'`           | The file can't be read without its password. Ask before you do anything else.      |
| `'optional'`           | The file is open, but its owner password would give more rights. You may offer it. |

`passwordPrompt.hint` says which password to ask for (`'user'` or `'owner'`), when the engine
knows. When the state is `'required'`, `passwordPrompt.incorrect` is `true` if the last password
you tried was wrong.

A locked document still opens: without a password, `open()` succeeds, and every call that reads
the file fails with `DocPasswordRequired` until you unlock it. A wrong password doesn't fail
`open()` either: the document opens locked, and `passwordPrompt.incorrect` is `true`, so you can
say "that password is wrong" and ask again.

`unlock()` fails with `DocPasswordIncorrect` when the password is wrong, so you can ask again:

```ts
import { EngineError, EngineErrorCode } from '@embedpdf/engine';

while (doc.security.passwordPrompt.state === 'required') {
  try {
    await doc.security.unlock({ password: await askUserForPassword() });
  } catch (error) {
    if (!EngineError.is(error, EngineErrorCode.DocPasswordIncorrect)) throw error;
    showMessage('That password is wrong. Try again.');
  }
}
```

### Owner password

A PDF can have two passwords. The user password opens it; the owner password also lifts the
file's own restrictions, such as "no printing" or "no copying". When a file is open but not with
its owner password, `passwordPrompt` is `'optional'`.

To accept only the owner password, pass `mode: 'owner'`. The user password then fails with
`DocPasswordIncorrect`:

```ts
await doc.security.unlock({ password, mode: 'owner' });
```

The file's own restrictions only count when the user's permissions include `pdf.permissions`;
see [Permissions](/docs/engine/concepts/permissions#the-pdfs-own-restrictions).

<Engine local>

## Who is working on it

Tell the engine who the user is and what they may do when you open the document:

```ts
const doc = await engine.open(
  { kind: 'bytes', bytes },
  {
    identity: { userId: 'u_381', displayName: 'Dana Smith', groupId: 'legal' },
    scope: [
      'doc.open',
      'doc.render',
      'doc.text.select',
      'doc.annotate.read',
      'annotations:create:self',
      'annotations:update:self',
      'annotations:delete:self',
    ],
  },
);
```

`identity` is who the user is. The engine writes three of its fields into what the user creates or
changes:

| Field         | What the engine does with it                                                                                    |
| :------------ | :-------------------------------------------------------------------------------------------------------------- |
| `displayName` | Becomes the annotation's `author`, the name PDF apps show, and a form field's `filledByName`.                   |
| `userId`      | Becomes the annotation's `userId`, `createdBy` and `modifiedBy`, and a form field's `createdBy` and `filledBy`. |
| `groupId`     | The group new annotations and form fields go in (`groupId`).                                                    |

The other fields (`email`, `title`, `organization`, `organizationalUnit`) never go into the PDF.
The viewer uses them in stamps and signature appearances.

`scope` is what the user may do, as a list of permissions. Leave it out and the user may do
everything; the engine logs a reminder once, so you notice before you ship. Permissions that
name the user's own annotations (`:self`) need an `identity` with a `userId` to check them
against: without one, `open()` fails with `InvalidArg`. A `:group=<id>` permission names its
group itself, so it needs no identity.

Read both back from `doc.security`:

```ts
doc.security.identity?.displayName; // 'Dana Smith'
doc.security.allows('doc.download'); // false
```

[Permissions](/docs/engine/concepts/permissions) lists every permission and how to build an
interface that matches them.

</Engine>

<Engine cloud>

On the cloud, who the user is and what they may do come from their token, so `open()` has no
options for them. See [What a token carries](/docs/engine/authentication#what-a-token-carries).

</Engine>

## Close a document

Close a document when you're done with it:

```ts
await doc.close();
```

<Engine local>

Closing frees the memory the document uses. After it, calls on `doc` fail with `DocNotOpen`.
Closing twice is fine.

</Engine>

<Engine cloud>

Closing stops the live updates for this document and forgets what the engine cached. The document
itself stays on the server. After it, calls on `doc` fail with `DocNotOpen`. Closing twice is
fine.

</Engine>

## One engine, many documents

Create one engine for your app and open every document on it:

```ts
const [contract, invoice] = await Promise.all([
  engine.open(contractInput),
  engine.open(invoiceInput),
]);
```

Keep the engine for as long as your app runs. When you do want to shut it down, at the end of a
script for example, destroy it; that also closes every document still open on it:

```ts
await engine.destroy();
```

<Engine local>

All documents on one engine share one copy of PDFium, so opening another document doesn't start
another worker. Each open document holds its file in memory until you close it.

</Engine>

<Engine cloud>

Each document keeps the token it was opened with, so one engine can hold documents opened for
different users or with different permissions.

</Engine>

## Good to know

<Engine local>

### Layers: keep your changes apart from the file

The engine can keep your changes apart from the original bytes: `downloadLayer()` saves just the
changes (a **layer**), and you open them later on top of the original. A layer holds only what you
changed, so it stays small however large the file is.

```ts
// Save only what changed
const changes = await doc.downloadLayer();

// Later: the original, with the changes on top
const reopened = await engine.open({
  kind: 'layerBytes',
  baseBytes: original,
  layer: { kind: 'artifact', bytes: changes },
});
```

Leave out `layer` (or pass `{ kind: 'fresh' }`) to start with no changes. `baseKey` is an id for
the original bytes; documents opened with the same `baseKey` share them in memory.

### Large files in Node

In Node, `layerFile` opens a file straight from disk. The engine reads the parts it needs when it
needs them, so the file never has to fit in memory:

```ts
const doc = await engine.open({ kind: 'layerFile', basePath: '/data/scan-4000-pages.pdf' });
```

The changes can live in a file too: `layer: { kind: 'artifact-file', path }`. `layerFile` needs
PDFium's native build; on the WebAssembly runtime it fails with `NotImplemented`.

### Your bytes after `open()`

In the browser, an `ArrayBuffer` you pass is transferred to the engine, not copied, so it's empty
afterwards. Pass a copy to keep yours, such as `buffer.slice(0)`.

</Engine>

### Cancelling an open

`open()` returns a promise you can cancel, like every call:

```ts
const opening = engine.open(input);
cancelButton.onclick = () => opening.abort();
```

See [Errors & cancelling](/docs/engine/concepts/errors-and-cancelling).

---

### Content Engine Documents Outline (`docs/content/engine/documents/outline.mdx`)

---
title: Outline
description: Read a document's outline (its bookmarks), and add, change, move and delete its items.
---

# Outline

A document's outline is its table of contents: titled items, nested, each going to a place in the
document. PDF apps show it in a side panel; Acrobat calls the items bookmarks.

```ts
const { items } = await doc.outline.get();
for (const item of items) console.log(item.title);

await doc.outline.create({ title: 'Appendix', target: { kind: 'goto', destination } });
```

## Read

`get()` lists every item, depth first: a parent before its children, siblings in their order. An
item names its `parent` (`null` at the top level), so you can build the tree from the list:

```ts
const { items } = await doc.outline.get();
const childrenOf = (parent: OutlineItemRef | null) =>
  items.filter((item) => item.parent?.objectNumber === parent?.objectNumber);

const chapters = childrenOf(null);
```

| Field    | What it is                                                                                   |
| :------- | :------------------------------------------------------------------------------------------- |
| `ref`    | The item's ref: `{ kind: 'objectNumber', objectNumber }`. It never changes.                  |
| `parent` | The item it sits under, or `null` at the top level.                                          |
| `title`  | Its title, as written.                                                                       |
| `target` | Where it goes, read the way a link's target is (see below), or `null` when it goes nowhere. |
| `action` | Its action as a tree, to run it the way a link's is, or `null`.                              |
| `open`   | Whether its children show when the document opens. Always `false` for an item without any.   |
| `color`  | The colour of its title, as `'#rrggbb'`, or `null` when the file sets none.                  |
| `bold`   | Whether its title is bold.                                                                   |
| `italic` | Whether its title is italic.                                                                 |

`get()` also says `incomplete: true` when part of the outline couldn't be read; the rest is
listed. See [What a read leaves out](#what-a-read-leaves-out).

## Where an item goes

`target` is the same shape as a link's: `goto` with a destination in
[page space](/docs/engine/concepts/coordinates), `uri` for a website, `named` for a viewer verb
such as `'NextPage'`, and `javascript`, `goto-remote`, `launch` or `unsupported` for the rest. An
item that goes to a named destination reads as the place that name points at.

```ts
if (item.target?.kind === 'goto') {
  const { destination } = item.target;
  console.log(destination.page, destination.kind); // the page's ref, and how close: 'xyz', 'fit', …
}
```

`action` is the item's whole action, for an item that runs one: a script, or several actions in a
row. Run it as you would a link's. An item that only names a place has a `target` and no
`action`.

## Add an item

`create()` adds an item and returns it, with its parent's children as they are now:

```ts
const { item, order } = await doc.outline.create(
  { title: 'Results', target: { kind: 'goto', destination: { kind: 'fit', page: page.ref } } },
  { parent: chapter.ref, position: { after: intro.ref } },
);
order.children; // the chapter's children, the new item among them
```

`parent` is the item it goes under (the top level when you leave it out), and `position` its
place among that parent's children: `'start'`, `'end'` (the default), `{ before: ref }` or
`{ after: ref }`. A position names a neighbour, not a number, so it keeps its meaning when other
items come and go.

What a new item takes:

| Field    | What it is                                                                                         |
| :------- | :------------------------------------------------------------------------------------------------- |
| `title`  | Its title. Up to 32,767 characters.                                                                |
| `target` | Where it goes: a `goto` destination, a `uri`, or one of `NextPage`, `PrevPage`, `FirstPage`, `LastPage`. |
| `color`  | `'#rrggbb'`, or leave it out for the viewer's own text colour.                                    |
| `bold`   | Bold title.                                                                                        |
| `italic` | Italic title.                                                                                      |

A new item has no children yet, so it isn't open. A parent or a neighbour the outline doesn't
have fails with `NotFound`.

## Change an item

`update()` changes the fields you pass and leaves the others. `null` removes a target or a
colour:

```ts
const { item } = await doc.outline.update(results.ref, {
  title: 'Results (revised)',
  color: '#c92a2a',
  target: null, // goes nowhere now
});
```

`open` sets whether an item's children show when the document opens. It takes effect only on an
item with children: a leaf has no open state, and reads `false`.

## Move items

`move()` moves items together, in the order you give them, each with the items under it:

```ts
const { orders } = await doc.outline.move([appendixA.ref, appendixB.ref], null, 'end');
```

`orders` lists the children of every parent the move changed: the parents the items left and the
one they went to. An item that loses its last child is no longer open.

A move is refused with `InvalidArg` when it would put an item under itself or under an item it
holds, names an item twice, names an item together with one under it, or uses one of the moved
items as the neighbour.

## Delete an item

`delete()` takes an item and every item under it:

```ts
const { meta } = await doc.outline.delete(chapter.ref);
meta.changed; // the chapter first, then the items under it
```

An undo brings the same items back, under the refs they had, at the place the item had. See
[Changes & undo](/docs/engine/documents/changes).

## In a change

Each call above is a one-op change. To make several edits one step of the history, put their ops
in one change:

| Op `type`        | Its fields                                     | The call it stands for                                       |
| :--------------- | :--------------------------------------------- | :----------------------------------------------------------- |
| `outline.create` | `parent`, `position`, `data`, `objectNumber`   | `doc.outline.create(data, { parent, position, objectNumber })` |
| `outline.update` | `ref`, `patch`, `expect`                       | `doc.outline.update(ref, patch)`                             |
| `outline.move`   | `refs`, `parent`, `position`                   | `doc.outline.move(refs, parent, position)`                   |
| `outline.delete` | `ref`, `expect`                                | `doc.outline.delete(ref)`                                    |

To add an item and items under it in one change, name the new item before it exists:

```ts
await doc.objectNumbers.reserve(1);
const objectNumber = doc.objectNumbers.take()!;
const chapter = { kind: 'objectNumber', objectNumber } as const;

await doc.apply({
  ops: [
    { type: 'outline.create', parent: null, data: { title: 'Chapter 3' }, objectNumber },
    { type: 'outline.create', parent: chapter, data: { title: '3.1 Method' } },
    { type: 'outline.update', ref: chapter, patch: { open: true } },
  ],
});
```

The last op opens the chapter: an item is open only once it has children, so the open state comes
after them. See [Refer to something an earlier op makes](/docs/engine/documents/changes#refer-to-something-an-earlier-op-makes).

## Permissions

Reading the outline needs `doc.open`. Changing it needs `doc.outline.modify`; without it, every
write fails with `Forbidden`.

A file's own permissions give `doc.outline.modify` when it allows changing the document, or
document assembly: Acrobat's assembly permission covers adding bookmarks. A certified document
can forbid changing the outline for everyone; a write then fails with `ProtectedDocument`. See
[What signatures forbid](/docs/engine/concepts/permissions#what-signatures-forbid).

## Good to know

### What a read leaves out

A read stops at 50,000 items and 128 levels deep, and skips what isn't an item: a loop in the
outline's links, or an entry that isn't a separate object in the file (with the items under it).
It then lists what it could read and says `incomplete: true`. Writes still work on the items it
lists.

### Pages that are gone

Deleting a page doesn't delete the items that go to it. Their `target` reads `unsupported` from
then on, so you can find them:

```ts
const lost = items.filter((item) => item.target?.kind === 'unsupported');
```

### Where it's stored

The outline is the file's `/Outlines` tree: each item an object with `/Title`, linked to its
parent and siblings (`/Parent`, `/First`, `/Next`, …). The engine keeps those links and each
item's `/Count` right on every write, and writes only what changed. A target is written as an
action (`/A`), as a link's is; a file's `/Dest` reads as the same `goto`.

### Events

Writes fire `outline.created`, `outline.updated`, `outline.moved` and `outline.deleted`, and an
undo of a delete fires `outline.restored`. Each event carries the call's result, so a parent's
`order` or a move's `orders` say where items are now. See [Events](/docs/engine/concepts/events).

---

### Content Engine Documents Pages (`docs/content/engine/documents/pages.mdx`)

---
title: Pages
description: List a document's pages, then rotate, reorder, delete, insert, extract and flatten them.
---

# Pages

List the pages of a document to learn their size and order, then work on them by `ref`:

```ts
const { pageCount, pages } = await doc.pages.list();

await doc.pages.rotate([pages[0].ref], 90); // turn the first page
await doc.pages.delete([pages[pageCount - 1].ref]); // delete the last one
```

## List pages

`doc.pages.list()` returns every page in display order. It doesn't load any page content, so it's
quick even for a long document.

```ts
const { pages } = await doc.pages.list();

for (const page of pages) {
  console.log(page.index, page.label, page.size.width, page.size.height, page.rotation);
}
```

| Field        | What it is                                                                                                                                                                |
| :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `index`      | Position in the document, from 0. Changes when pages move.                                                                                                                |
| `ref`        | The page's address. Stays the same when pages move; pass it to every page call.                                                                                           |
| `label`      | The page's label from the PDF (`'iii'`, `'A-1'`), or `null` when the file has none.                                                                                       |
| `size`       | `{ width, height }` of the visible area (the crop box) in points, before rotation.                                                                                        |
| `rotation`   | `0`, `90`, `180` or `270`: how far the page is turned clockwise when shown.                                                                                               |
| `userUnit`   | How large one point is, in multiples of 1/72 inch. `1` for almost every file.                                                                                             |
| `tabOrder`   | The order its annotations and form fields take keyboard focus in: `'rows'`, `'columns'`, `'structure'`, `'annotations'` or `'widgets'`; `null` when the page doesn't say. |
| `boxes`      | The page's five boxes: `media`, `crop`, `bleed`, `trim` and `art`. See below.                                                                                             |
| `pdfCropBox` | Where the page sits in the PDF file's own numbers, for tools that use those. See below.                                                                                   |

`boxes` follows the PDF standard. A box the file leaves out takes its default: the crop box is the
media box, and the bleed, trim and art boxes are the crop box. Every box stays inside the media
box, so a crop box that reaches past it is cut back. `crop` is what the page shows, and `size` is
its size.

Each box is `{ x, y, width, height }`, measured in points from the top-left corner of what the
page shows, with y going down. So `boxes.crop` is always `{ x: 0, y: 0, width, height }`, and a
media box that reaches past the page's top-left corner has a negative `x` and `y`:

```ts
{
  size: { width: 512, height: 672 },
  boxes: {
    media: { x: -50, y: -60, width: 612, height: 792 },
    crop: { x: 0, y: 0, width: 512, height: 672 },
    trim: { x: 10, y: 10, width: 492, height: 652 },
    // bleed and art
  },
  pdfCropBox: { left: 50, bottom: 60, right: 562, top: 732 },
}
```

`pdfCropBox` is the only value in the PDF file's own numbers: the crop box where the file places
it, as `{ left, bottom, right, top }` with y going up. You need it only beside a tool that reads
those numbers, such as iText, PDFBox or Acrobat JavaScript. `pdfRectOf()` and `pdfPointOf()` turn a
box or point into them, and `pageBoxOf()` and `pagePointOf()` turn them back. `pdfDestinationOf()`
and `pageDestinationOf()` do the same for a destination, with the crop box of the page it goes to:

```ts
import { pdfRectOf } from '@embedpdf/engine';

const inTheFile = pdfRectOf({ x: 50, y: 82, width: 50, height: 50 }, page.pdfCropBox);
// { left: 100, right: 150, bottom: 600, top: 650 }
```

`size` is the page before it's turned. For a page at `90` or `270`, swap width and height to get
the size it's shown at:

```ts
const { size, rotation } = pages[0];
const turned = rotation === 90 || rotation === 270;
const shown = turned ? { width: size.height, height: size.width } : size;
```

## Refs, not positions

Every call that works on pages takes refs, never indexes. A ref stays valid when pages move,
rotate, or when pages before it are inserted or deleted, so you can keep it for as long as the
page exists.

```ts
const page = doc.page(ref); // a handle for one page: render, text, annotations
```

`doc.page(ref)` makes no request. When you stored a page's object number yourself,
`toPageRef(12)` turns it back into a ref. See [Refs & identity](/docs/engine/concepts/identity).

## Rotate

`rotate()` sets how pages are turned: `0`, `90`, `180` or `270` degrees clockwise. It sets the
rotation, it doesn't add to it, so one value works for several pages at once:

```ts
await doc.pages.rotate([pageA.ref, pageB.ref], 90);
```

To turn a page a further quarter, add 90 to its current `rotation` from `list()`. After 270,
start again at 0.

Rotating changes how the page is shown, not its content, so annotations and text keep their
positions. Renders don't turn by themselves: pass the page's `rotation` to show it turned (see
[Turn the page](/docs/engine/documents/rendering#turn-the-page)).

## Reorder

`reorder(refs, position)` takes the pages out and puts them back together, in the order you pass
them, at `position`: right after or before another page, or at the start or the end.

```ts
const { pages } = await doc.pages.list();
const last = pages[pages.length - 1].ref;

await doc.pages.reorder([last], 'start'); // the last page to the front
await doc.pages.reorder([pages[0].ref], { after: pages[1].ref }); // swap the first two
```

A position names a page, never a number, so it means the same when other pages come or go
before the call lands. The page you name can't be one of the pages you move (`InvalidArg`);
one the document doesn't have fails with `NotFound`.

## Delete

```ts
await doc.pages.delete([page.ref]);
```

A page's annotations go with it. Outline items that go to it stay, and their target reads
`unsupported` from then on (see [Outline](/docs/engine/documents/outline#pages-that-are-gone)). A
document keeps at least one page: deleting every page fails with `InvalidArg`.

## Insert another PDF

`insert(bytes, position?)` copies every page of another PDF into this one, with their
annotations. Without a position, the pages go at the end:

```ts
const response = await fetch('/appendix.pdf');
const { insertedPages } = await doc.pages.insert(await response.arrayBuffer());
```

`insertedPages` holds the refs of the new pages, in order. To merge several files, insert them one
after the other.

## Insert blank pages

`insertBlank()` adds empty pages of the size you give, in points:

```ts
// Two US Letter pages after the first page
const { pages } = await doc.pages.list();
const letter = { width: 612, height: 792 };
await doc.pages.insertBlank({ size: letter, count: 2 }, { after: pages[0].ref });
```

A4 is `{ width: 595, height: 842 }`. `count` is 1 by default and at most 100 per call.

## Extract pages to a new PDF

`extract(refs)` returns a new PDF with copies of those pages, in the order you pass them. The
document itself doesn't change.

```ts
const bytes = await doc.pages.extract([pages[0].ref, pages[1].ref]);
```

That makes splitting a document a loop, one file per page:

```ts
for (const { ref, index } of pages) {
  const bytes = await doc.pages.extract([ref]);
  await save(`page-${index + 1}.pdf`, bytes);
}
```

And taking pages from one document into another is `extract()` then `insert()`:

```ts
const bytes = await source.pages.extract([ref]);
await target.pages.insert(bytes, 'start');
```

### A copy as the pages print

With `flatten`, the copy is the pages as they look: the annotations and form fields painted into
the pages, and nothing left to click, fill in or edit. It takes the same options as
[`flatten()`](#flatten-a-pages-annotations), so `{ usage: 'print' }` makes a copy for paper: only
what prints is on it, and layers are in their print state. The document itself doesn't change.

```ts
const printable = await doc.pages.extract(refs, { flatten: { usage: 'print' } });
```

`includeAnnotations` and `includeFormFields` choose what's painted in. Each defaults to what the
caller may read, as in a [page picture](/docs/engine/documents/rendering), and they only go with
`flatten`:

```ts
await doc.pages.extract(refs, { flatten: { usage: 'print' }, includeAnnotations: false });
```

The copy keeps the document's title, so a print queue or a saved file has a name.

## Flatten a page's annotations

Flattening draws a page's annotations into the page itself and removes them, so they can't be
changed or deleted anymore. Filled-in form fields become plain content the same way.

```ts
const { results } = await doc.pages.flatten([page.ref]);
// results: [{ page, status: 'applied' }]
```

Each page's `status` is `'applied'`, or `'unchanged'` when it had nothing to flatten. Flattening is
all or nothing: when a page fails, or the call is cancelled, no page changes and the call rejects
with the error. Flattening paints what the page shows: an annotation without a stored appearance is
painted as the engine draws it. Hidden annotations and popups stay as they are, and so do the few
annotations the engine draws only from a stored appearance, such as a stamp without one.
Flattening can't be undone, and it ends undo for every change made before it; see
[Changes & undo](/docs/engine/documents/changes#what-cant-be-undone).

By default the page is flattened as it's shown on screen, which leaves out annotations shown only
when printed. Pass `{ usage: 'print' }` to flatten it as it prints, which leaves out annotations that
don't print:

```ts
await doc.pages.flatten([page.ref], { usage: 'print' });
```

New annotations print unless you create them with `print: false`; see
[Drawing annotations yourself](/docs/engine/annotations/appearances#what-prints).

To flatten only some annotations, see
[Drawing annotations yourself](/docs/engine/annotations/appearances).

## Permissions

| Call                                                             | Needs                                        |
| :--------------------------------------------------------------- | :------------------------------------------- |
| `list()`                                                         | `doc.open`                                   |
| `rotate()`, `reorder()`, `delete()`, `insert()`, `insertBlank()` | `doc.pages.assemble`                         |
| `extract()`                                                      | `doc.download`: the pages leave the document |
| `extract()` with `flatten`                                       | `doc.download` or `doc.download.flattened`   |
| `flatten()`                                                      | `doc.pages.modify` and `doc.annotate.modify` |

See [Permissions](/docs/engine/concepts/permissions).

## Good to know

### What a change returns

`rotate()`, `reorder()`, `delete()`, `insert()`, `insertBlank()`, `setName()` and `removeName()`
return the new page list as `layout`, the same shape `list()` returns, so you can replace the list
you hold without asking again:

```ts
const { layout } = await doc.pages.reorder([ref], 'start');
showThumbnails(layout.pages);
```

Other code that shows pages can listen for `pages.reordered`, `pages.rotated`, `pages.deleted`,
`pages.inserted`, `pages.restored` and `pages.flattened` instead. See
[Events](/docs/engine/concepts/events).

### Undo

`rotate()`, `reorder()`, `delete()`, `insert()` and `insertBlank()` are changes: each returns its
`opId` in `meta`, and `doc.apply({ undoOf: opId })` undoes it. An undo of a delete brings the same
pages back, under the same refs, with their annotations and names, and fires `pages.restored`. To
make several page edits one step, apply them as one change. See
[Changes & undo](/docs/engine/documents/changes).

```ts
const { meta } = await doc.pages.delete([ref]);
await doc.apply({ undoOf: meta.opId }); // the page is back where it was
```

### Inserting

A password-protected PDF can't be inserted: `insert()` fails with `DocPasswordRequired`. Open it,
unlock it, and `extract()` its pages instead. Pages you insert get new refs; the pages already in
the document keep theirs.

<Engine local>

In the browser, an `ArrayBuffer` you pass to `insert()` is transferred to the engine, not copied,
so it's empty afterwards. Pass a copy to keep yours.

</Engine>

### Named pages

A PDF can give pages names. Acrobat's stamp files work this way: each page is one stamp, named
`identifier=label`. The names come with the page list:

```ts
const { namedPages } = await doc.pages.list();
// [{ name: 'Approved=Approved', target: { kind: 'page', page: ref } }, …]
```

`target` is `{ kind: 'page', page }` for a page in the document. A name can also point at a hidden
page template (`{ kind: 'template' }`), which isn't a page you can show, or at nothing
(`{ kind: 'dangling' }`) in a file another app made.

Add, change or remove a name with `setName()` and `removeName()`. The page itself stays:

```ts
await doc.pages.setName({ name: 'Draft=Draft', page: page.ref });
await doc.pages.setName({ name: 'Draft=Concept', page: page.ref, replace: 'Draft=Draft' }); // rename
await doc.pages.removeName({ name: 'Draft=Concept' });
```

Both need `doc.pages.assemble`. Deleting a page removes its names.

---

### Content Engine Documents Redaction (`docs/content/engine/documents/redaction.mdx`)

---
title: Redaction
description: Mark areas to remove, apply the redactions to take the content out of the document for good, and save a file without it.
---

# Redaction

Redaction removes content from a document for good: the text, images and drawings under an area
are taken out of the file, not just covered. It takes two steps. First you mark what to remove,
then you apply the marks:

```ts
// 1. Mark: a redact annotation over the area
const { annotation } = await page.annotations.create({
  subtype: 'redact',
  rect: { x: 72, y: 132, width: 248, height: 20 },
  interiorColor: '#000000',
});

// 2. Apply: remove what's under it
await doc.redaction.apply({ annotations: [annotation.ref] });

// 3. Save a file without the removed content
const bytes = await doc.download({ mode: 'rewrite' });
```

## Mark what to remove

A mark is a `redact` annotation. It removes nothing yet: it shows as an outline, the text under it
can still be read and copied, and you can change or delete it like any other annotation. That
lets someone review the marks before anything is gone.

Mark an area with `rect`, in points. To mark text exactly, as it runs across lines or at an
angle, give its `quadPoints` instead, the same corners a highlight takes. Search results have
them:

```ts
const { matches } = await doc.search.query({ text: 'Jane Doe', wholeWord: true });

for (const match of matches) {
  await doc.page(match.page).annotations.create({
    subtype: 'redact',
    quadPoints: match.segments.map((segment) => segment.quad),
    interiorColor: '#000000',
  });
}
```

With `quadPoints`, the quads decide what's removed, and `rect` (the box around them) is worked out
for you. A text selection gives you the same quads; see
[Selection & geometry](/docs/engine/text/selection-and-geometry).

## Choose what's left in its place

After the content is removed, the engine paints the area. These fields say what it paints:

| Field           | What it is                                                                                                | Default       |
| :-------------- | :-------------------------------------------------------------------------------------------------------- | :------------ |
| `interiorColor` | The fill, `'#rrggbb'`                                                                                     | no fill       |
| `overlayText`   | Text on top of the fill, such as `'REDACTED'`                                                             | none          |
| `repeat`        | Repeat `overlayText` to fill the whole area                                                               | `false`       |
| `fontFamily`    | The font of `overlayText`, such as `'times-roman'`; see [Fonts](/docs/engine/annotations/free-text#fonts) | `'helvetica'` |
| `fontSize`      | Its size in points; `0` fits the text to the area                                                         | `12`          |
| `fontColor`     | Its color                                                                                                 | black         |
| `textAlign`     | `'left'`, `'center'` or `'right'`                                                                         | `'left'`      |
| `color`         | The outline shown while the area is only marked                                                           | red           |

Without a fill or text, the area is left blank: the content is gone and the page shows through.
In PDF apps that support it, hovering a mark previews the fill and text it will leave.

## Apply

`apply()` removes the content under marks. Pass the marks to apply:

```ts
await doc.redaction.apply({ annotations: [markA.ref, markB.ref] });
```

Or apply every mark on some pages:

```ts
const { pages } = await doc.pages.list();
await doc.redaction.apply({ pages: pages.map((page) => page.ref) });
```

The result says what happened on each page:

```ts
const { results, removedAnnotationCount } = await doc.redaction.apply({ pages: [page.ref] });
// results: [{ page, status: 'applied', removedAnnotationCount: 1 }]
```

A page's `status` is `'applied'`, or `'unchanged'` when it had no marks. The marks you applied are
removed from the page. Other annotations that overlap a marked area are removed too, because they
could reveal what was there; `removedAnnotationCount` counts those, so you can tell the user.

Passing a ref that isn't a redact annotation fails with `InvalidArg` before anything changes.

## What gets removed

Under each marked area, applying removes:

- **Text**: the characters themselves, so they can't be searched, selected or copied anymore.
  Text next to the area stays.
- **Images**: the pixels inside the area are erased from the image. For a slanted mark, that's
  the box around it.
- **Drawings**: the parts of lines, shapes and fills inside the area.
- **Annotations and form fields** that overlap the area, even in part, with their popups. For a
  mark with `quadPoints`, the quads count, not the box around them.

This includes content the page draws from shared parts of the file, not only what the page draws
itself. There's no undo: once applied, the content is gone from the document. Applying also ends
undo for every change made before it; see
[Changes & undo](/docs/engine/documents/changes#what-cant-be-undone).

## Save the redacted file

Apply changes the document as it is now, but an incremental save still holds the version you
opened, with everything you removed. Anyone with a PDF tool can recover it. For a file you share,
save with `mode: 'rewrite'`:

```ts
const bytes = await doc.download({ mode: 'rewrite' });
```

A rewrite writes the document as it is now and nothing else. See
[Saving](/docs/engine/documents/saving#incremental-or-rewrite).

<Engine local>

<CloudPdfCallout
  title="Keep the original on your server"
  href="https://www.cloudpdf.com/docs/engine"
  cta="Redact with CloudPDF"
>

On the local engine, redaction runs in the browser, on a copy of the original the user already
has. With CloudPDF it runs on your server, and the original never leaves it: people who may only
see the redacted version get a token without `doc.download`, and receive the pages as images and
the text that's left, never the file.

</CloudPdfCallout>

</Engine>

<Engine cloud>

The server keeps the file as it was uploaded, so an incremental download of a redacted document
contains the original. Give `doc.download` only to people who may see the original. Everyone else
sees the pages and the text as they are after the redaction, and never receives the file.

</Engine>

## Permissions

Marking is annotation work: creating a redact annotation needs the permission to create
annotations. Applying needs all three of `doc.redact`, `doc.pages.modify` and
`doc.annotate.modify`, because it changes the page and removes annotations. Without them,
`apply()` fails with `Forbidden`. See [Permissions](/docs/engine/concepts/permissions).

Give `doc.redact` to the people who decide what goes; others can still mark areas for them.

## Good to know

### Marks from other apps

A redact annotation made by another app (Acrobat, for example) applies the same way. When it
carries its own finished overlay, the engine paints that; otherwise it paints the fill and text
from the fields above.

### When a page fails

Apply is all or nothing. When a page fails, or the call is cancelled, no page changes and the call
rejects with the error. Apply again to retry.

### Events

Other code can follow redactions with the `redaction.applied` event. See
[Events](/docs/engine/concepts/events).

---

### Content Engine Documents Rendering (`docs/content/engine/documents/rendering.mdx`)

---
title: Rendering
description: Render a page, or part of one, as an image at the size you need, and map points between the image and the page.
---

# Rendering

`page.render.image()` draws a page as an image you can show in an `<img>`:

```ts
const image = await page.render.image({ viewport: { kind: 'width', width: 800 } });
const { url, revoke } = await image.objectUrl();

img.src = url;
// When the image is no longer shown:
revoke();
```

## Choose the size

`viewport` sets how big the image is. Give a width in pixels, and the height follows the page's
shape:

```ts
await page.render.image({ viewport: { kind: 'width', width: 1200 } });
```

Or give a scale, in pixels per PDF point. At scale `1`, a US Letter page (612 × 792 points) is 612
× 792 pixels:

```ts
await page.render.image({ viewport: { kind: 'scale', scale: 2 } });
```

Without `viewport`, the scale is `1`.

### Sharp images on high-density screens

A screen with a `devicePixelRatio` of 2 shows two pixels for every CSS pixel. Render that many
more pixels and show the image at its CSS size:

```ts
const cssWidth = 800;
const image = await page.render.image({
  viewport: { kind: 'width', width: Math.round(cssWidth * devicePixelRatio) },
});

img.style.width = `${cssWidth}px`;
```

## Turn the page

An image shows the page as it's stored, unturned, whatever the page's own `rotation`. That way
one image works for every way you show the page: a viewer turns the view, not the image. To get
the page the way PDF apps show it, pass the page's own rotation from `doc.pages.list()`:

```ts
const { pages } = await doc.pages.list();

await doc.page(pages[0].ref).render.image({
  viewport: { kind: 'width', width: 800 },
  rotation: pages[0].rotation,
});
```

`rotation` is `0` (the default), `90`, `180` or `270` degrees clockwise, from the page as it's
stored. At `90` and `270`, the image's width and height swap, and `viewport.width` is the width
of the turned image.

## Format and quality

<Engine local>

`format` is `'png'` (the default), `'webp'`, or `'bmp'`. BMP is uncompressed: the fastest to make
and the largest, for images you only keep in memory. A BMP image's `format` reads `'bmp'`.

In Node there is no canvas, so the engine encodes PNG and BMP itself. WebP fails with
`NotImplemented` there unless you give the engine your own `imageEncoder`; see
[Setup](/docs/engine/setup#nodejs).

</Engine>

<Engine cloud>

`format` is `'webp'` (the default) or `'png'`. The server keeps WebP renders at the widths it
shares between users (see [Render policy](#render-policy)), so WebP is usually the fastest to load.

</Engine>

`quality` goes from `0` to `1` and applies to WebP. Any other value fails with `InvalidArg`:

```ts
await page.render.image({ viewport: { kind: 'width', width: 800 }, format: 'webp', quality: 0.8 });
```

The image tells you what you got:

| Field             | What it is                                         |
| :---------------- | :------------------------------------------------- |
| `format`          | `'png'` or `'webp'`                                |
| `contentType`     | The MIME type, such as `'image/webp'`              |
| `width`, `height` | The image's size in pixels                         |
| `objectUrl()`     | A URL for `<img>`, and `revoke()` to free it       |
| `blob()`          | The image's bytes, as a `Blob` (below)             |
| `transform`       | Converts between its pixels and page space (below) |

## The image's bytes

`blob()` gives the image as a `Blob`, typed with its `contentType`. It's the same call on every
engine, so use it to upload the image, save it or decode it:

```ts
const image = await page.render.image({ viewport: { kind: 'width', width: 800 }, format: 'png' });
const blob = await image.blob();

const bitmap = await createImageBitmap(blob);
```

Cancel it with `.abort()`, like a render.

<Engine local>

The image's `source` is `{ kind: 'bytes', bytes }`: the same bytes, without making a `Blob`.

</Engine>

<Engine cloud>

The image's `source` is `{ kind: 'url', url }`, an address that needs the document's token.
`blob()` fetches it with the token, and asks again when the document changed in the meantime.

</Engine>

## Background and annotations

The page is drawn on white. For a transparent background, pass `background: 'transparent'` with
PNG or WebP.

Annotations are drawn on the page, form fields included, each in the state it shows: the page as
it is shown on screen. To draw the page without them, when you draw annotations yourself, pass
`includeAnnotations: false`:

```ts
await page.render.image({ includeAnnotations: false, background: 'transparent', format: 'png' });
```

When you draw only the form fields yourself, over the page, leave out just those with
`includeFormFields: false`. The other annotations stay in the picture:

```ts
await page.render.image({ includeFormFields: false });
```

And the other way round: when you draw the annotations yourself, ask for the form fields without
them:

```ts
await page.render.image({ includeAnnotations: false, includeFormFields: true });
```

Hidden form fields are never drawn, and fields marked not to show on screen are drawn only in a
[print picture](#print-pictures).

The picture holds only what the user may read. Left out, `includeAnnotations` draws the
annotations when the user may read them (`doc.annotate.read`), and `includeFormFields` the form
fields when they may read the form (`doc.forms.read`). So someone who may fill a form but not read
its comments gets a picture without the comments. Asking for annotations or fields the user may
not read fails with `Forbidden`, naming the option.

## Print pictures

A picture for paper follows the print rules instead of the screen's. Pass `usage: 'print'`, and
size it with a `dpi` viewport:

```ts
const image = await page.render.image({
  usage: 'print',
  viewport: { kind: 'dpi', dpi: 300 },
  format: 'png',
});
```

What changes on paper:

- **Layers take their print state.** A watermark set to show only when printing is drawn, and a
  layer set to show only on screen is not.
- **Annotations and form fields are drawn by their `print` flag.** One that doesn't print is left
  out, even when it shows on screen, such as a Print or Submit button marked "visible but doesn't
  print". One that is hidden on screen but prints (`noView`) is drawn. Hidden ones never are.

`dpi` is `150` or `300`, in dots per inch of the paper the page prints on, A4 or Letter. A page
that fits on that paper is drawn at its own size at that DPI. A bigger one gets no more pixels than
it needs shrunk to fit the paper, since the printer would throw the rest away: an A0 drawing at
300 DPI is 2482 × 3508 pixels, not 9933 × 14043.

A print picture is always the whole page, upright, on white. It takes only a `dpi` viewport, and
no `target`, `rotation` or `background`; a `dpi` viewport is only for print pictures. Anything else
fails with `InvalidArg`. `includeAnnotations` and `includeFormFields` work as for any picture.

`pageTransform()` gives a print picture's size and transform without rendering:

```ts
const { width, height } = pageTransform(pages[0], { viewport: { kind: 'dpi', dpi: 150 } });
```

Printing needs `doc.print`, and 300 DPI also `doc.print.high`: the PDF's permission to print at
full quality. Without them the call fails with `Forbidden`.

<Engine cloud>

Each print picture has an address of its own for each DPI, so a CDN only serves the 300 DPI
pictures to sessions that may print at full quality.

</Engine>

## Part of a page

`target` renders one area instead of the whole page. The rect is in page space, from the page's
top-left like annotation rects and text positions, so you can render exactly the area of an
annotation:

```ts
const closeUp = await page.render.image({
  target: { kind: 'rect', rect: annotation.rect },
  viewport: { kind: 'scale', scale: 4 },
});
```

`viewport` then sizes the area: a `width` is the width of the area's image.

<Engine local>

## Raw pixels

`render.raw()` takes the same options (except `format` and `quality`) and returns the pixels
themselves, for a canvas or your own image pipeline:

```ts
const raster = await page.render.raw({ viewport: { kind: 'width', width: 800 } });

const imageData = new ImageData(new Uint8ClampedArray(raster.data), raster.width, raster.height);
canvas.width = raster.width;
canvas.height = raster.height;
canvas.getContext('2d')!.putImageData(imageData, 0, 0);
```

`raster.data` is RGBA, four bytes per pixel, not premultiplied, row after row (`raster.stride`
bytes per row). It skips encoding, so it's the fastest way to get a page onto a canvas.

</Engine>

## From image pixels to page points

The engine gives every position in page space, in points from the page's top-left corner: text,
search matches, annotations. The pointer and the screen work in pixels. A page transform converts
between the two, and every image has the one it was rendered with:

```ts
const image = await page.render.image({ viewport: { kind: 'width', width: 800 } });

img.addEventListener('click', (event) => {
  const scale = image.width / img.clientWidth; // image pixels per CSS pixel
  const point = image.transform.pixelsToPage({
    x: event.offsetX * scale,
    y: event.offsetY * scale,
  });
  // point is { x, y } in page space, ready for layout.charAt() or an annotation
});

const box = image.transform.pageToPixels(match.segments[0].rect); // { x, y, width, height }
```

`pageToPixels()` takes a point, a box or a quad; `pixelsToPage()` takes a point or a box. Both
take the page's rotation, the size and the `target` into account, so you never do that math
yourself.

<Engine local>

A raw raster has a `transform` too.

</Engine>

### Without an image

You don't need an image to get a transform. `pageTransform()` computes the same one from the
page's layout and the settings you would render with:

```ts
import { pageTransform } from '@embedpdf/engine';

const { pages } = await doc.pages.list();
const view = pageTransform(pages[0], { viewport: { kind: 'width', width: 800 } });

view.width; // 800: the size an image with these settings has
view.height; // 1035 for a US Letter page
```

It takes the same settings as `render.image()` (`viewport`, `rotation`, `target`) and gives the
exact numbers of the image those settings render, rounding included. Use it to place boxes before
the image arrives, to keep an overlay in place while you zoom, for a page drawn as several tiles
(one transform for the whole page), or for a page you draw yourself.

### With CSS or a canvas

`view.matrix` is the conversion from page space to pixels as the six numbers of a CSS `matrix()`
or a canvas `setTransform()`. Draw an overlay in page space and let the browser place it:

```tsx
<svg width={view.width} height={view.height}>
  <g transform={`matrix(${view.matrix.join(' ')})`}>
    {match.segments.map(({ rect }, i) => (
      <rect
        key={i}
        x={rect.x}
        y={rect.y}
        width={rect.width}
        height={rect.height}
        fill="rgba(255, 215, 0, 0.4)"
      />
    ))}
  </g>
</svg>
```

### Turned pages

Rotation is one of the settings, `0` by default as for a render: at `90` the page turns
clockwise, the width and height swap, and the transform turns with it. Positions in page space don't change when a page turns, so a
selection or a search result keeps the same numbers; only the transform is different.

## Cancel a render

Rendering a large page takes time. When the user scrolls away before the image arrives, cancel
it:

```ts
const pending = page.render.image({ viewport: { kind: 'width', width: 1600 } });

// Later, when the page is no longer visible:
pending.abort();
```

The promise then fails with `Aborted`, and the engine stops drawing the page, usually within a few
milliseconds, so the renders waiting behind it start right away. See
[Errors & cancelling](/docs/engine/concepts/errors-and-cancelling).

## Options

| Option               | What it does                                                                                                    | Default                                                                  |
| :------------------- | :-------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- |
| `viewport`           | `{ kind: 'width', width }` in pixels, or `{ kind: 'scale', scale }`; `{ kind: 'dpi', dpi }` for a print picture | scale `1`                                                                |
| `rotation`           | Turns the image clockwise: `0`, `90`, `180` or `270`                                                            | `0`                                                                      |
| `format`             | `'png'` or `'webp'` (see [Format and quality](#format-and-quality))                                             | see above                                                                |
| `quality`            | WebP quality, from `0` to `1`                                                                                   | encoder default                                                          |
| `background`         | `'white'` or `'transparent'`                                                                                    | `'white'`                                                                |
| `includeAnnotations` | Draw the page's annotations                                                                                     | `true` when the user may read them                                       |
| `includeFormFields`  | Draw the form fields too                                                                                        | `false` without annotations; else `true` when the user may read the form |
| `target`             | `{ kind: 'page' }`, or `{ kind: 'rect', rect }` for one area                                                    | the whole page                                                           |
| `usage`              | `'display'`, or `'print'` for a [print picture](#print-pictures)                                                | `'display'`                                                              |

## Permissions

Rendering needs the `doc.render` permission. Without it, the call fails with `Forbidden`. A
picture with annotations also needs `doc.annotate.read`, and one with form fields
`doc.forms.read`. A print picture also needs `doc.print`, and at 300 DPI `doc.print.high`.

## Good to know

### Render policy

`doc.render.getPolicy()` says which render sizes this document renders best at:

```ts
const policy = await doc.render.getPolicy();
```

<Engine local>

The local engine answers `{ kind: 'continuous' }`: it renders any size exactly as you ask.

To test a lattice policy locally, pass `localEngine({ renderPolicy })`.

</Engine>

<Engine cloud>

A CloudPDF server answers a **lattice**: a short list of page widths it renders once, stores and
shares with every user of the document (for example `[320, 640, 1280, 2560]`). Other widths
render too; they just aren't stored and shared.

```ts
{
  kind: 'lattice',
  fullPage: { widths: [320, 640, 1280, 2560] },
  formats: ['webp'],
  background: 'white',
  enforced: false,
}
```

With `enforced: true`, the server refuses full-page renders at other widths with `InvalidArg`.

</Engine>

The engine never changes the size you ask for. To pick the lattice width for the size you want,
use `snapFullPageViewport()`. It returns the smallest width on the lattice that's at least as
large (or the largest one), and your viewport unchanged under a continuous policy:

```ts
import { snapFullPageViewport } from '@embedpdf/engine';

const { pages } = await doc.pages.list();
const viewport = snapFullPageViewport(
  policy,
  { kind: 'scale', scale: 2 },
  {
    pageWidth: pages[0].size.width,
  },
);
await doc.page(pages[0].ref).render.image({ viewport });
```

The [render plugin](/docs/headless/viewing/render) does all of this for you, and adds tiles when
the user zooms in far.

<Engine cloud>

### When the image downloads

`render.image()` resolves as soon as the engine knows the image's address. The image itself
downloads when you call `objectUrl()` or `blob()`, so an image you never show costs nothing. Renders are cached
on the server with the page, and change only when the page or its annotations do.

</Engine>

---

### Content Engine Documents Saving (`docs/content/engine/documents/saving.mdx`)

---
title: Saving
description: Get the PDF with your changes as bytes, save it in the browser or in Node, and choose between an incremental save and a full rewrite.
---

# Saving

`doc.download()` returns the whole PDF, with every change you made, as bytes:

```ts
const bytes = await doc.download();
```

<Engine cloud>

You don't need to save to keep a change: the server stores each change when the call returns.
`download()` is for when you want the file itself. See [What the server keeps](#what-the-server-keeps).

</Engine>

## Save in the browser

Wrap the bytes in a `Blob` and let the browser download it:

```ts
const bytes = await doc.download();

const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));
const link = document.createElement('a');
link.href = url;
link.download = 'contract-signed.pdf';
link.click();
URL.revokeObjectURL(url);
```

To send the file to your server instead, put the same `Blob` in a `FormData` or pass the bytes to
`fetch()` as the body.

## Save in Node

```ts
import { writeFile } from 'node:fs/promises';

await writeFile('out.pdf', await doc.download());
```

<Engine local>

For large files, `downloadToFile()` writes the PDF straight to disk, without the bytes passing
through JavaScript:

```ts
await doc.downloadToFile('out.pdf');
```

It takes the same `mode` option as `download()`, and works in Node only.

</Engine>

## Incremental or rewrite

`mode` chooses how the file is written:

```ts
const bytes = await doc.download({ mode: 'rewrite' });
```

| Mode                      | What you get                                                                  |
| :------------------------ | :---------------------------------------------------------------------------- |
| `'incremental'` (default) | The file as it was opened, byte for byte, with your changes added at the end. |
| `'rewrite'`               | A new file with only the document as it is now. Earlier versions are gone.    |

Pick by what the file is for:

- **Signed documents: incremental.** A signature covers the bytes as they were signed. An
  incremental save leaves those bytes alone, so the signatures stay valid. A rewrite would break
  every one of them, so a signed document refuses it with `ProtectedDocument`.
- **After a redaction, or after deleting something sensitive: rewrite.** An incremental file
  still holds the version from before your changes, and PDF tools can recover it. Only a rewrite
  leaves the removed content out. See [Redaction](/docs/engine/documents/redaction).
- **The smallest file: rewrite.** It also leaves out anything nothing refers to anymore.
- **Everything else: incremental.** It's the default, it's fast, and it never changes the parts of
  the file you didn't touch.

<Engine cloud>

## What the server keeps

The server stores the file as it was uploaded, and never changes it. Your changes are stored
beside it, as they happen, and everyone who opens the document sees them.

`download()` puts the two together into one PDF. With `'incremental'`, that's the uploaded file
with your changes added at the end; with `'rewrite'`, a new file without the earlier version.

Because the uploaded file is always inside an incremental download, give `doc.download` only to
people who may see the document as it was uploaded. Everyone else can view it without downloading
it: see [Permissions](/docs/engine/concepts/permissions).

</Engine>

<Engine local>

## Where your changes are

The engine keeps the file as you opened it, and never changes it. Your changes live beside it, in
memory, until you download. When you close the document without downloading, they're gone.

</Engine>

## Permissions

Downloading needs the `doc.download` permission. Without it, `download()` fails with `Forbidden`,
so check before you show a download button:

```ts
if (doc.security.allows('doc.download')) {
  showDownloadButton();
}
```

## Good to know

### An unchanged document

When nothing changed since you opened the document, an incremental download returns the exact
bytes you opened. The same holds when you changed something and then changed it back, such as an
annotation added and deleted again. That keeps a signed document exactly as it was signed.

### Password-protected files

A file that had a password when you opened it still has it after you save, in both modes.

<Engine local>

### Saving only the changes

`doc.downloadLayer()` returns only what you changed, as a small file you can store next to the
original and open with it later. See
[Layers](/docs/engine/documents/opening#layers-keep-your-changes-apart-from-the-file).

</Engine>

### Cancelling a download

`download()` returns a promise you can cancel with `.abort()`, like every call. See
[Errors & cancelling](/docs/engine/concepts/errors-and-cancelling).

---

### Content Engine Forms Authoring (`docs/content/engine/forms/authoring.mdx`)

---
title: Creating form fields
description: Add text fields, checkboxes, radio groups, dropdowns, lists, buttons and signature fields to a PDF, change or delete them, and repair a broken form.
---

# Creating form fields

`create()` adds a field to the document and places it on a page, in one call:

```ts
const { field } = await doc.forms.create({
  family: 'text',
  name: 'customer.email',
  maxLength: 60,
  widgets: [{ page: page.ref, rect: { x: 72, y: 168, width: 228, height: 24 } }],
});

await doc.forms.setValue(field.ref, { value: 'ada@example.com' });
```

The new field fills in, exports and shows like any other field; see
[Reading & filling forms](/docs/engine/forms).

## Fields and widgets

A form has two parts. The **field** holds the value: it has a name, a family and a few settings,
and it belongs to the document, not to a page. A **widget** is where the field shows: the box on
a page that you see and click.

A widget is an annotation of kind `'widget'`, but it's the form's: `doc.forms.list()` lists it
in `widgets`, and you place, move and style it through the form. Most fields have one widget. A radio group has one per button, and a
field can have widgets on several pages that all show the same value.

## Create a field

`create()` takes the family, the field's full name and its settings. `widgets` places it on the
pages, one entry per place it shows; leave it out to create a field that isn't on any page yet,
and [add a widget](#add-a-widget) later.

| Field           | What it is                                                                                                                                                | Default    |
| :-------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------- |
| `family`        | `'text'`, `'checkbox'`, `'radio'`, `'combobox'`, `'listbox'`, `'pushbutton'` or `'signature'`                                                             |            |
| `name`          | The full name. Dots make groups: `customer.email` creates `customer` if it's new.                                                                         |            |
| `widgets`       | Where it shows: a list of `{ page, rect }`, `rect` in points from the page's top-left, each with the [widget's look](#the-widgets-look).                  | not placed |
| `readOnly`      | PDF apps don't let the user change it.                                                                                                                    | `false`    |
| `required`      | PDF apps ask for a value before the form is submitted.                                                                                                    | `false`    |
| `noExport`      | Left out when the form is submitted.                                                                                                                      | `false`    |
| `alternateName` | The tooltip PDF apps show, also read by screen readers.                                                                                                   | none       |
| `mappingName`   | A second name, for other apps that export the form's data.                                                                                                | none       |
| `groupId`       | The group it goes in: who fills it in. Another group than your own takes `fields:set-group` for it (see [Signers and groups](/docs/engine/forms/groups)). | your own   |

`create()` is one change: when any part of the draft is invalid, it fails with `InvalidArg` (or
`NotFound` for a page that isn't there) and creates nothing. A name that's already taken fails
with `InvalidArg` too.

Each family adds its own settings:

| Family       | Settings                                                                                               |
| :----------- | :----------------------------------------------------------------------------------------------------- |
| `text`       | `defaultValue`, `maxLength`, `multiline`, `password`, `comb` (one character per box, with `maxLength`) |
| `checkbox`   | Each widget's `exportValue`: what the form data holds when it's checked. Default `'Yes'`.              |
| `radio`      | Each widget's `exportValue` (one widget per button), `noToggleToOff`, `radiosInUnison`                 |
| `combobox`   | `options`, `defaultValue`, `edit` (the user may also type their own text)                              |
| `listbox`    | `options`, `multiSelect`, `defaultValue` (option values; more than one needs `multiSelect`)            |
| `pushbutton` | Only widgets, each with its `caption`. See [A push button](#a-push-button).                            |
| `signature`  | Only widgets. Signing it is on [Digital signatures](/docs/engine/forms/signatures).                    |

### A radio group

A radio group has one widget per button. Each button's `exportValue` is the value you choose it
with, so it can't be `'Off'`:

```ts
const { field } = await doc.forms.create({
  family: 'radio',
  name: 'plan',
  noToggleToOff: true,
  widgets: [
    {
      page: page.ref,
      rect: { x: 72, y: 136, width: 16, height: 16 },
      exportValue: 'monthly',
    },
    { page: page.ref, rect: { x: 72, y: 166, width: 16, height: 16 }, exportValue: 'yearly' },
  ],
});

await doc.forms.setValue(field.ref, { value: 'monthly' });
```

### A dropdown

Options are `{ label, value }`: the user sees the `label`, the form stores the `value`.

```ts
await doc.forms.create({
  family: 'combobox',
  name: 'country',
  options: [
    { label: 'Netherlands', value: 'NL' },
    { label: 'Belgium', value: 'BE' },
  ],
  defaultValue: 'NL',
  widgets: [{ page: page.ref, rect: { x: 72, y: 252, width: 148, height: 20 } }],
});
```

### A push button

A push button holds no value: it does something when it's clicked. Give each widget its
`caption` and what a click does, in `actions` (see [Widget actions](#widget-actions)):

```ts
await doc.forms.create({
  family: 'pushbutton',
  name: 'clear',
  widgets: [
    {
      page: page.ref,
      rect: { x: 400, y: 700, width: 120, height: 28 },
      caption: 'Clear form',
      interiorColor: '#f6f8fa',
      actions: { activate: { type: 'reset-form', fields: null, exclude: false } },
    },
  ],
});
```

The engine draws the button: its background, its border, and the caption centred in the widget's
font. `setValue()` refuses a push button with `InvalidArg`. A button that only goes to a page,
resets the form or shows and hides fields needs `doc.forms.modify` alone; one that runs a script,
submits the form or opens a web address needs `doc.forms.script` too.

### The widget's look

Next to `page` and `rect`, each entry of `widgets` takes the widget's style. These are the same
fields you change later with `doc.forms.updateWidget()`:

| Field           | What it is                                                                                       |
| :-------------- | :----------------------------------------------------------------------------------------------- |
| `color`         | Border color, `'#rrggbb'`                                                                        |
| `interiorColor` | Background color                                                                                 |
| `strokeWidth`   | Border width in points                                                                           |
| `borderStyle`   | `'solid'`, `'dashed'`, `'beveled'`, `'inset'` or `'underline'` (a line under the field)          |
| `dashArray`     | A dashed border's dash and gap lengths in points, such as `[3, 2]`                               |
| `fontFamily`    | A standard PDF font, such as `'helvetica'` or `'courier'`, or the `key` of a font you registered |
| `fontSize`      | Text size in points                                                                              |
| `fontColor`     | Text color                                                                                       |
| `textAlign`     | `'left'`, `'center'` or `'right'`                                                                |
| `caption`       | A push button's caption; other widgets take none                                                 |
| `checkStyle`    | A checkbox's or radio button's mark; other widgets take none                                     |

```ts
widgets: [
  {
    page: page.ref,
    rect: { x: 72, y: 208, width: 228, height: 24 },
    color: '#1f6feb',
    interiorColor: '#f6f8fa',
    fontSize: 10,
  },
],
```

A mark is `'check'`, `'cross'`, `'star'`, `'circle'`, `'square'` or `'diamond'`. With
`checkStyle: null`, a checkbox draws a check and a radio button a circle.

A font the document already has, and that you didn't register, reads as its family name, such as
`'Roboto'`. Changing a widget's color or size keeps its font; to change the font, name a standard
font or a registered one.

To change the look later, update the widget with the same fields (see
[Move and style widgets](#move-and-style-widgets)). An entry also takes what the widget does when
it's clicked, in `actions` (see [Widget actions](#widget-actions)), and a quarter turn of its
contents, in `rotation` (see [Turned widgets](#turned-widgets)).

## Change a field

`update()` takes the field's `ref` and only the settings you want to change:

```ts
await doc.forms.update(field.ref, { required: true, maxLength: 80 });
```

A setting you leave out keeps its value. `null` removes one: `{ maxLength: null }` for no limit,
`{ defaultValue: null }` for no default, `{ alternateName: null }` for no tooltip.

You can change the settings in the tables above, except the family and the widgets. A signature
field takes the settings every field has: `name`, `readOnly`, `required`, `noExport`,
`alternateName` and `mappingName`.
Also:

- `name` renames the last part of the name: `customer.email` renamed to `'contact'` becomes
  `customer.contact`. A name with dots, or one a sibling already has, fails with `InvalidArg`. A
  field can't move to another group.
- `options` replaces the whole list of a dropdown or list.
- `maxLength` fails with `InvalidArg` when the current text is longer.

## Scripts and calculations

A field can run scripts: while the user types (`keystroke`), to show the value (`format`), to
check it (`validate`) and to work it out from other fields (`calculate`). `create()` and `update()`
take them in `actions`:

```ts
const { field: total } = await doc.forms.create({
  family: 'text',
  name: 'total',
  readOnly: true,
  actions: {
    calculate: {
      type: 'javascript',
      script: 'event.value = this.getField("subtotal").value * 1.21;',
    },
    format: { type: 'javascript', script: 'AFNumber_Format(2, 0, 0, 0, "€ ", true);' },
  },
});

await doc.forms.update(total.ref, { actions: { format: null } }); // remove one script
```

A script sets that event's, `null` removes it, and an event you leave out keeps its script. A
field's events take JavaScript only: any other action fails with `InvalidArg`. A script can name
more scripts to run after it, in `next`.

The engine stores the scripts; it doesn't run them. The viewer's form plugin runs them while the
user fills the form in (see
[Calculations, formatting and validation](/docs/engine/forms#calculations-formatting-and-validation)).

### The calculation order

Calculate scripts run in the form's calculation order, which `doc.forms.list()` returns as
`calculationOrder`. A field joins it at the end when it gets a calculate script, and leaves it when
the script is removed or the field is deleted. `reorderCalculations()` moves fields in it, the way
`reorderWidgets()` restacks widgets:

```ts
const { calculationOrder } = await doc.forms.reorderCalculations([tax.ref], { before: total.ref });
```

A field must be in the order to move it (`NotFound` otherwise). A result of `create()`, `update()`
or `delete()` that changed the order carries the new one as `calculationOrder`.

## Delete a field

`delete()` removes the field and all of its widgets from their pages:

```ts
await doc.forms.delete(field.ref);
```

## Add a widget

To show a field in one more place, give `addWidget()` the place, the same shape as an entry of
`widgets` in `create()`:

```ts
const { field: updated } = await doc.forms.addWidget(field.ref, {
  page: page.ref,
  rect: { x: 300, y: 112, width: 150, height: 20 },
});
```

The field now shows in both places, with the same value. For a radio group, pass the new
button's `exportValue` in the same object; a checkbox widget gets `'Yes'` when you leave it out.

## Delete a widget

`deleteWidget()` deletes a widget: it leaves its page, and its field when it has one. It resolves
the field as it is now, or `null` for a widget that was in no field:

```ts
const { field: updated } = await doc.forms.deleteWidget(widget.ref);
```

The field stays. Without widgets it isn't on any page (`widgets` is empty), until you add one
again. To take a field off its pages entirely, delete the field. A widget that is its field's own
dictionary (see [Good to know](#good-to-know)) is refused with `InvalidArg`: delete the field.

`removeWidget()` only takes a widget out of its field. It stays on the page with the look it had,
as a widget in no field (its `field` is `null`) that holds no value, until `deleteWidget()` takes
it off:

```ts
await doc.forms.removeWidget(field.ref, widget.ref);
```

A widget is the form's, so `page.annotations.delete()` refuses one with `InvalidArg`.

## Move and style widgets

`updateWidget()` takes the widget's `ref` and the fields to change, the way
`page.annotations.update()` takes an annotation's. The engine redraws the widget:

```ts
const { widget: moved } = await doc.forms.updateWidget(widget.ref, {
  rect: { x: 300, y: 128, width: 180, height: 24 },
  interiorColor: '#fff7db',
});
```

The style fields are the ones in [The widget's look](#the-widgets-look). A widget's flags, such
as `hidden` and `print`, work as for other annotations; see
[Annotations](/docs/engine/annotations#flags). To stop the user changing a field, set the
field's `readOnly`: PDF apps ignore a widget annotation's own `readOnly`.

A widget is the form's, so `page.annotations.update()` refuses one with `InvalidArg`.

### Turned widgets

A widget's contents can turn a quarter, so that a field on a turned page reads upright, or a label
runs up the page. A widget then reads like every shape that turns: its `box` is the frame its
contents are laid out in, its `rotation` the turn (0, 90, 180 or 270, clockwise; `null` upright),
and its `rect` where it stands on the page, the box turned. A placement takes the turn, and either
the `rect` or the `box`, as a create of any shape that turns does:

```ts
await doc.forms.create({
  family: 'text',
  name: 'side',
  // 24 wide and 160 tall: the text runs up the page
  widgets: [{ page: page.ref, rect: { x: 20, y: 120, width: 24, height: 160 }, rotation: 270 }],
});

// The same widget, by the frame its text runs in
widgets: [{ page: page.ref, box: { x: -48, y: 188, width: 160, height: 24 }, rotation: 270 }];
```

A placement with both takes the `box`; one with neither fails with `InvalidArg`.

An update turns a widget as it turns any shape:

- `{ rotation }` turns the whole widget about its middle, so it stands the other way;
- `{ rotation, box }` turns its contents and keeps it where it is, as Acrobat's Orientation does.
  The new `box` is the `rect` with its sides swapped;
- `{ rect }` moves and resizes it, and keeps its turn.

Another angle fails with `InvalidArg`. The turn is the widget's `/MK /R`, which every PDF app
reads. The file holds it counterclockwise, so Acrobat's Orientation of 90° reads as
`rotation: 270`.

### Stacking order

Widgets are drawn above every annotation, later ones over earlier ones. `reorderWidgets()`
restacks the widgets of one page: they go together, in the order you pass them, right after or
before another widget of the page, or to the bottom (`'start'`) or the top (`'end'`):

```ts
const { order } = await doc.forms.reorderWidgets([widget.ref], { after: other.ref });
```

`order` is the page's widgets in their new order, as refs. Where a page's tab order follows the
order of its widgets, this changes the tab order too. An annotation is never a neighbour here:
`reorderWidgets()` refuses one with `InvalidArg`; restack annotations with
[`page.annotations.reorder()`](/docs/engine/annotations#delete-and-reorder).

## Widget actions

A widget can do something when the user clicks it (`activate`), and on its other events: the
pointer entering or leaving it (`cursorEnter`, `cursorExit`), the mouse button going down and up
(`mouseDown`, `mouseUp`), getting and losing focus (`focus`, `blur`), and its page opening,
closing, coming into view or leaving it (`pageOpen`, `pageClose`, `pageVisible`,
`pageInvisible`). `updateWidget()` takes them in `actions`, and so does each entry of `widgets` in
`create()` and `addWidget()`:

```ts
await doc.forms.updateWidget(widget.ref, {
  actions: {
    activate: { type: 'reset-form', fields: null, exclude: false },
    focus: { type: 'javascript', script: 'app.alert("Your name as on your passport");' },
  },
});

await doc.forms.updateWidget(widget.ref, { actions: { focus: null } }); // remove one
```

An action sets that event's, `null` removes it, and an event you leave out keeps its action;
`actions: null` removes them all. An action runs a script (`javascript`), goes to a page (`goto`,
with a destination as for [links](/docs/engine/annotations/links#link-to-a-page)), opens a web
address (`uri`), runs a viewer command (`named`), shows or hides fields (`hide`), or resets or
submits the form (`reset-form`, `submit-form`). More actions to run after it go in `next`.

The widget's row in `doc.forms.list()` reads them back in `actions`, by event, each as the full
action. A row's `actions` passed back unchanged keeps them; to change one, pass it as above.

On a field stored in one object with its widget (see [Good to know](#good-to-know)), the widget's
actions and the field's scripts stay apart: writing one leaves the other as it is.

## Repair a broken form

Some PDFs ship a form that other PDF apps read differently: fields that show on a page but are
missing from the form's list of fields, or no form at all around them. The engine reads such
fields anyway ([recovered fields](/docs/engine/forms#recovered-fields)); `repair()` fixes the
file, so every PDF app sees the same form:

```ts
const report = await doc.forms.repair({ bakeAppearances: true });
```

| Field                     | What it counts                                                     |
| :------------------------ | :----------------------------------------------------------------- |
| `acroformCreated`         | `true` when the document had no form and now has one.              |
| `fieldsLinked`            | Recovered fields added to the form's list of fields.               |
| `widgetsLinked`           | Stray widgets reconnected to their field.                          |
| `fieldsUnrepairable`      | Fields stored inside another object, which can't be linked.        |
| `appearancesBaked`        | Widgets that got a freshly drawn look.                             |
| `needsAppearancesCleared` | `true` when PDF apps no longer need to draw the fields themselves. |

With `bakeAppearances: true` (default `false`), the engine also draws every widget that has no
look of its own. When the form asks PDF apps to draw its fields themselves (`needsAppearances` on
the [list](/docs/engine/forms#read-the-form)), it redraws every widget and clears that request,
so the form looks the same everywhere.

`repair()` only changes what needs fixing. Run it twice and the second report is all zeros. A
repair can't be undone, and it ends undo for every change made before it; see
[Changes & undo](/docs/engine/documents/changes#what-cant-be-undone).

## Events

| Event                                      | Fired by                                   |
| :----------------------------------------- | :----------------------------------------- |
| `forms.created`                            | `create()`                                 |
| `forms.updated`                            | `update()`, and `setSignatureAppearance()` |
| `forms.deleted`                            | `delete()`                                 |
| `forms.widgetAdded`, `forms.widgetRemoved` | `addWidget()`, `removeWidget()`            |
| `forms.widgetDeleted`                      | `deleteWidget()`                           |
| `forms.widgetUpdated`                      | `updateWidget()`                           |
| `forms.widgetsReordered`                   | `reorderWidgets()`                         |
| `forms.calculationsReordered`              | `reorderCalculations()`                    |
| `forms.repaired`                           | `repair()`                                 |

Each event carries what the call returned, with the rows of the widgets it changed (`widgets`). See
[Events](/docs/engine/concepts/events).

## Permissions

Creating, changing and deleting fields, adding, moving, restacking and removing widgets and
repairing the form need `doc.forms.modify`, which includes `doc.forms.fill` and `doc.forms.read`.
Writing a script, or a widget action that submits the form or opens a web address, also needs
`doc.forms.script`: it runs for everyone who fills the form in (see
[Scripts in forms](/docs/engine/concepts/permissions#scripts-in-forms)). Removing one doesn't, and
widget actions that go to a page, reset the form or show and hide fields need `doc.forms.modify`
alone. See [Permissions](/docs/engine/concepts/permissions).

A certified document doesn't allow changes to its fields: these calls fail with
`ProtectedDocument`. See
[Digital signatures](/docs/engine/forms/signatures#protection-what-signatures-allow).

## Good to know

- **A document without a form.** The first `create()` gives the document a form. A document with
  no form reports `formKind: 'none'`; after `create()` it's `'acroform'`.
- **Field and widget in one object.** Many PDFs store a field with a single widget as one object.
  Such a widget can't be removed from its field (`removeWidget()` fails with `InvalidArg`); delete
  the field instead. Fields the engine creates always keep the two apart.
- **Widgets come with the form.** `field.widgets` names each widget (`page` and `ref`), and the
  form's `widgets` holds each one's row, with its `field`. `page.annotations.list()` never returns
  a widget.
- **Checked looks in the file.** How a checkbox or radio widget's `exportValue` relates to its
  look in the file is in
  [Checkboxes with several widgets](/docs/engine/forms#checkboxes-with-several-widgets).
- **Names and groups.** `billing.name` and `billing.email` are two fields in a group `billing`.
  `list()` returns the two fields, not the group, and an import makes the group again from the
  names.

---

### Content Engine Forms Groups (`docs/content/engine/forms/groups.mdx`)

---
title: Signers and groups
description: Let several people fill in and sign one form, each only their own fields, by putting fields in groups and giving each signer the permissions for theirs.
---

# Signers and groups

A contract has the buyer's part and the seller's part. The buyer fills in and signs the buyer's
fields, the seller the seller's, and once the buyer has signed, nobody changes what the buyer
filled in. Groups do this: each field belongs to a group, and each user's permissions say which
groups they fill in and sign.

```ts
const { fields } = await doc.forms.list();
fields.map((field) => [field.name, field.groupId]);
// [['buyer.name', 'buyer'], ['buyer.signature', 'buyer'], ['seller.name', 'seller'], …]
```

## Put fields in groups

A field's group is its `groupId`. Give it when you create the field, or change it later:

```ts
await doc.forms.create({
  family: 'text',
  name: 'buyer.name',
  groupId: 'buyer',
  widgets: [{ page, rect: { x: 72, y: 120, width: 200, height: 24 } }],
});
await doc.forms.update(field.ref, { groupId: 'seller' });
```

A field you create without a `groupId` goes in your own group (`identity.groupId`). Putting it in
another group takes `fields:set-group` for that group, so a designer who prepares the contract
gets one for each party:

```ts
const designer = [
  'doc.open',
  'doc.render',
  'doc.forms.modify',
  'fields:set-group:group=buyer',
  'fields:set-group:group=seller',
];
```

A group can be changed, never removed. It's a setting of the field: it's saved in the PDF (in the
field's `/EMBD_Metadata`), other PDF apps ignore it, and a form [export](/docs/engine/forms/import-export)
carries it.

## Give each signer their group

Each signer gets the permissions to fill in and sign their own group's fields:

```ts
const buyer = ['doc.open', 'doc.render', 'fields:fill:group=buyer', 'fields:sign:group=buyer'];
const seller = ['doc.open', 'doc.render', 'fields:*:group=seller'];
```

<Engine local>

```ts
const doc = await engine.open(
  { kind: 'bytes', bytes },
  { scope: buyer, identity: { userId: 'u_381', displayName: 'Dana Smith' } },
);
```

</Engine>

<Engine cloud>

Put them in each signer's token, with the signer's identity. A share link for "whoever is the
seller" needs no identity: its permissions name the group.

</Engine>

Every user who may read the form sees every field and every value: groups decide who changes a
field, not who sees it. When one signer must not see another's answers, give each their own
document.

Leave `doc.forms.modify` and `pdf.permissions` out of a signer's permissions. Designing the form
can delete and recreate any field, and `pdf.permissions` gives what the PDF file allows to
everyone, which can include designing it.

## Filling in

A signer fills in their fields as anyone does. A field of another group fails with `Forbidden`,
naming the permission it would take:

```ts
await doc.forms.setValue(buyerName.ref, { value: 'Dana Smith' });
await doc.forms.setValue(sellerName.ref, { value: 'x' });
// Forbidden: details.required is 'fields:fill:group=seller'
```

A field a script calculates, such as a total, may be in another group or in none. Write it in
the same change as the field it's calculated from, and it's allowed:

```ts
await doc.apply({
  ops: [
    { type: 'forms.setValue', field: price.ref, value: { value: '1200' } },
    { type: 'forms.setValue', field: total.ref, value: { value: '1452' } },
  ],
});
```

A reset that names a field the signer may not fill in is refused, and so is a reset of the whole
form, so pass their own: `doc.forms.reset(myFields)`. A [values import](/docs/engine/forms/import-export#fill-in-from-a-bundle)
leaves out the other fields, with the reason `fill-not-allowed`.

## Signing

A signer signs their own signature field, and only theirs:

```ts
const prepared = await doc.signatures.prepare({ field: buyerSignature.ref });
```

When the signature field is in a group and you name no `lock`, signing locks the rest of the
group: every field in it as it is when the buyer signs, except its signature fields. After that,
writing one of them fails with `ProtectedDocument` for everyone, in this engine and in other PDF
apps. Name a [`lock`](/docs/engine/forms/signatures#lock-fields) to choose the fields yourself.

The engine doesn't order the signers. Give the seller their permissions when the buyer is done.

## Build the interface from it

Ask before you show a control:

```ts
doc.security.allowsField('fill', field); // may the user fill this field in?
doc.security.allowsField('sign', field); // …sign this signature field?
doc.security.allowsField('set-group', { groupId: 'buyer' }); // …put fields in the buyer's group?
```

To show whose fields are the user's before they pick one, ask for every field at once:

```ts
const rights = doc.security.fieldRights();
rights.fill; // { all: true }, or { all: false, groups: ['buyer'] }
rights.sign; // the same, for signature fields
rights.setGroup; // the groups the user may put a field in
```

`{ all: true }` is every field. Otherwise only the fields in `groups` are the user's; a field in
no group isn't, and an empty list is no field at all.

The engine checks every write the same way, so these answers never disagree with a call. See
[Permissions](/docs/engine/concepts/permissions#form-fields).

---

### Content Engine Forms Import Export (`docs/content/engine/forms/import-export.mdx`)

---
title: Import & export
description: Take form fields out of a document as one bundle, then copy them into another document or fill its fields from it, in one call.
---

# Import & export

Export takes form fields out of a document as one **bundle**: each field whole, with its widgets,
its value, its scripts and actions, and who made and filled it in. A bundle goes back in two
ways:

```ts
const bundle = await source.forms.export();

// The design: new fields, with their widgets, values and scripts.
const { fields, dropped } = await target.forms.import(bundle);

// Only the values: the document's own fields of the same name are filled in.
const filled = await other.forms.importValues(bundle);
```

Use `import()` to put a form on a new version of a document, or to build a form once and reuse
it. Use `importValues()` to fill a form in from answers you stored, or to carry answers from one
copy of a form to another.

## Export

```ts
const everything = await doc.forms.export();
const some = await doc.forms.export({ fields: [toFieldRef('billing.name'), total.ref] });
const onPage = await doc.forms.export({ pages: [pageRef] }); // every field with a widget there
```

| Option   | What it is                        | Default |
| :------- | :-------------------------------- | :------ |
| `fields` | The fields to export.             | all     |
| `pages`  | The pages whose fields to export. | all     |

A field always comes whole: with every widget it has, whatever page each one is on. A field or a
page the document doesn't have refuses the export with `NotFound`.

Two things never travel:

- a signature: a signed signature field is exported unsigned;
- the value of a field marked `noExport`, and who filled it in.

## Store it as a file

`FormTransfer` writes a bundle as one JSON text, and reads it back, as
[`AnnotationTransfer`](/docs/engine/annotations/import-export#store-it-as-a-file) does for
annotations:

```ts
import { FormTransfer } from '@embedpdf/engine';

const text = FormTransfer.stringify(bundle);
const again = await FormTransfer.parse(text);
```

`parse()` checks the [limits](#limits) first, then the format, the version and the shape. Anything
else is refused with `InvalidArg`.

## Copy the design

```ts
const result = await doc.forms.import(bundle);

result.fields; // the new fields, in bundle order
result.refMap; // [{ from, to }]: each field's ref in the bundle, and its ref here
result.dropped; // what was left out, and why
result.calculationOrder; // the form's whole calculation order, when the import changed it
```

| Option        | What it is                                                                  | Default     |
| :------------ | :-------------------------------------------------------------------------- | :---------- |
| `pages`       | Which page each bundle page goes to.                                        | `'same'`    |
| `attribution` | `'restore'` keeps who made and filled each field; `'stamp'` makes it yours. | `'restore'` |
| `values`      | Whether each field gets the value the bundle holds.                         | `true`      |
| `opId`        | A name for this import: its events carry it, and it names its undo.         | a new id    |

Everything is checked before anything is written. The import is all or nothing: a failure while
writing leaves the document exactly as it was, with no new fields and no events.

Each field is made as [`create()`](/docs/engine/forms/authoring) makes it, with every setting
the bundle has: its widgets with their place, look, flags and actions, its options, its default,
its scripts. Parent fields are made bare from the names, as a create makes them.

### Pages

Pages are placed as an annotation import places them: by default each widget goes to the page
with the same ref, and `pages` takes `'by-position'` or a list of `{ from, to }` pairs. A page a
widget is on, or an action goes to, that maps nowhere fails the import with `InvalidArg`. See
[Pages](/docs/engine/annotations/import-export#pages).

### Who made and filled it

With `attribution: 'restore'`, the default, each field keeps who created it and when, and who
last filled it in and when (`createdBy`, `createdAt`, `filledBy`, `filledByName`, `filledAt`).
The user who imports is recorded in `importedBy`. That writes fields in other people's names, so
it needs `doc.forms.import` beside `doc.forms.modify`.

With `attribution: 'stamp'`, each field is made as `create()` makes it: the user who imports
created it, and filled it in when it gets a value.

A field keeps its group (`groupId`) with `'restore'`. With `'stamp'`, it goes in the group it had,
when the user may put fields there (their own, or one they have `fields:set-group` for), and in
their own group when it had none. A group they may not set refuses the import.

### Scripts and actions

A field's scripts and its widgets' actions come along. A script, a submit or a link
(JavaScript, submit-form or URI) needs `doc.forms.script`: without it, each one is left out and
listed in `dropped`, and the field is imported without it. The actions that only change the view
(go to a page, reset the form, show or hide) come along with `doc.forms.modify` alone.

### The calculation order

The fields with a calculate script join the end of the form's
[calculation order](/docs/engine/forms/authoring), in the order the bundle has them. A field
whose calculate script was left out has no place in it.

### What's left out

What an import can't carry it leaves out, lists in `dropped`, and imports the rest. Each entry
names the field as the bundle does (`ref`), and a `widget` when it's about one widget. An entry
with a `field` means only that member was left out, such as `actions.calculate`: the field was
imported without it.

| `reason`             | What was left out                                                                                                        |
| :------------------- | :----------------------------------------------------------------------------------------------------------------------- |
| `name-conflict`      | A field whose full name the document has, or an earlier field of the bundle, or that would be a field's parent or child. |
| `no-field`           | A widget whose field isn't in the bundle.                                                                                |
| `unsupported-family` | A field of a kind the engine doesn't model.                                                                              |
| `script-not-allowed` | A script, a submit or a link, without `doc.forms.script`.                                                                |
| `unsupported-action` | Actions the engine can't write, or that were read incomplete.                                                            |

Because of `name-conflict`, importing the same bundle twice doesn't duplicate the fields.

## Fill in from a bundle

```ts
const result = await doc.forms.importValues(bundle);

result.fields; // the fields whose value changed, as they are now
result.dropped; // what was left out, and why
```

| Option        | What it is                                                          | Default     |
| :------------ | :------------------------------------------------------------------ | :---------- |
| `attribution` | `'restore'` keeps who filled each field in; `'stamp'` makes it you. | `'restore'` |
| `opId`        | A name for this import: its events carry it, and it names its undo. | a new id    |

Each field the bundle holds a value for fills the document's field of the same full name, as
`setValue()` would. A field that already holds that value is left as it is, and isn't in
`fields`. The import runs no scripts: no keystroke, format, validate or calculate script runs, as
for every value the engine writes.

| `reason`            | What was left out                                                              |
| :------------------ | :----------------------------------------------------------------------------- |
| `no-field`          | The document has no field of that name.                                        |
| `wrong-family`      | The document's field of that name is of another kind.                          |
| `value-not-allowed` | A value the field can't take: an option it doesn't have, a button it lacks.    |
| `locked`            | A field a signature locked.                                                    |
| `fill-not-allowed`  | A field the user may not fill in: its group isn't one their permissions reach. |

All of this is decided before the first value is written.

## Events

`import()` fires one `forms.created` event per field, and `importValues()` one `forms.valueSet`
per field it filled, all with the same `origin.tx`, so you can update your interface once for the
whole batch. See
[Which write an event came from](/docs/engine/concepts/events#which-write-an-event-came-from).

## Undo an import

Each import is one change. `doc.apply({ undoOf })` with its `opId` takes it back, and the undo's
undo brings it back again:

```ts
const imported = await doc.forms.import(bundle);
const undo = await doc.apply({ undoOf: imported.meta.opId });
await doc.apply({ undoOf: undo.meta.opId }); // back again
```

- The undo of `import()` deletes each field it made, with its widgets and its place in the
  calculation order. The redo brings back the same fields, with the same refs.
- The undo of `importValues()` puts back each field's value and who filled it in, as the undo of a
  `setValue()` does.

A field that was changed since stays, and its item is `skipped`; the rest of the import goes. As
for every change, only the user who imported may undo it, and a final write ends undo for the
imports before it. See [Changes & undo](/docs/engine/documents/changes).

## Limits

Form bundles have the [same limits](/docs/engine/annotations/import-export#limits) as annotation
bundles, with fields and widgets together counted as items. Past a limit, the call fails with
`PayloadTooLarge`, and `err.details.limit` names the limit.

<Engine cloud>

An import is one request. Its `opId`, made for you when you don't pass one, makes a retry safe: a
second request with the same `opId` returns what the first one did and writes nothing.

An export is cached until the document's form or pages change, so exporting again is cheap.

</Engine>

## Permissions

| Call                                             | Needs                                                                         |
| :----------------------------------------------- | :---------------------------------------------------------------------------- |
| `export()`                                       | `doc.forms.read` and `doc.download`                                           |
| `import(bundle, { attribution: 'stamp' })`       | `doc.forms.modify`; `doc.forms.script` to keep scripts                        |
| `import(bundle)`                                 | `doc.forms.modify` and `doc.forms.import`; `doc.forms.script` to keep scripts |
| `importValues(bundle, { attribution: 'stamp' })` | `doc.forms.fill`, or `fields:fill` for a group                                |
| `importValues(bundle)`                           | that, and `doc.forms.import`                                                  |

A values import leaves out the fields the user may not fill in (`fill-not-allowed`). Undoing an
import takes what the import took. See [Permissions](/docs/engine/concepts/permissions).

## Good to know

### What's in a bundle

```ts
bundle.fields; // [{ data }, …]: each field as a read returns it
bundle.widgets; // [{ data, resources: {} }, …]: each widget as the form's rows read it
bundle.calculationOrder; // the calculation order among the fields
bundle.pages; // [{ page, position, size }, …]
```

A bundle is plain data: the fields in the form's order, then their widgets in their fields'
order. A widget is drawn from its data, so a form bundle carries no bytes yet.

### The round trip

Export a form, import the bundle into a copy of its pages, and export that: when nothing was
dropped, you get the same bundle. Only the refs change, and the attribution: `importedBy` with
`'restore'`, who created and filled each field with `'stamp'`. The same document also exports to
the same bundle on the local and the cloud engine.

### Not FDF or XFDF

FDF and XFDF carry a form's values. A bundle carries what the engine reads: every setting of a
field and its widgets, their scripts and actions, and who made and filled each field. Its values
fill a form in through `importValues()`.

---

### Content Engine Forms Signatures (`docs/content/engine/forms/signatures.mdx`)

---
title: Digital signatures
description: Check who signed a PDF and what changed since, sign a signature field with your own key or signing service, and certify a document.
---

# Digital signatures

A digital signature seals a PDF as it was at the moment of signing. The engine reads every
signature in a document, tells you what changed after it, and signs a field for you. It never
sees your private key: it hands you the fingerprint of the file (a digest), your key signs it,
and the engine writes the result into the PDF.

```ts
import { sign, webCryptoSigner } from '@embedpdf/core-signature';

const { signatures } = await doc.signatures.list();
const field = signatures.find((signature) => !signature.signed)!.field;

await sign(doc, {
  field,
  key: webCryptoSigner({ privateKey, certificateChain }),
  signer: { name: 'Ada Lovelace', reason: 'Approved' },
});
```

Signing needs `doc.sign`, and `doc.forms.read` to find the field with `list()`; see
[Permissions](#permissions).

The cryptography (building a signature from a key, checking one against its certificate) lives
in its own package, which works with both engines:

```sh npm2yarn
npm install @embedpdf/core-signature
```

## Sign a field

`sign()` signs a field in one call. It gets the digest from the engine, has your `key` sign it,
checks the result and writes it into the document. When anything fails, it cancels, and the
document is as it was.

```ts
import { sign, webCryptoSigner } from '@embedpdf/core-signature';

const result = await sign(doc, {
  field,
  key: webCryptoSigner({ privateKey, certificateChain }),
  signer: { reason: 'Approved', location: 'Amsterdam' },
});

result.signature; // the new signature, as list() returns it
```

`key` is what signs the digest, one of:

| Key                                                 | Use it when                                                               |
| :-------------------------------------------------- | :------------------------------------------------------------------------ |
| `webCryptoSigner({ privateKey, certificateChain })` | You hold a WebCrypto key and its certificates (DER, the signer's first).  |
| `remoteSigner({ sign })`                            | A signing service or HSM turns a digest into a finished signature (CMS).  |
| `createTestSigner()`                                | Tests and demos: a throwaway key with a self-signed certificate.          |
| `createTestTimestampAuthority()`                    | Tests and demos: a throwaway [timestamp authority](#document-timestamps). |

After signing, the document is the signed file: `list()` shows the new signature, and
`doc.download()` returns the signed PDF. When the signature your key returns doesn't match what
the engine prepared, `sign()` fails with `SignatureRefused` and the message says why.

### Options

`sign()` and `prepare()` take these options (`sign()` also takes the `key`):

| Option         | What it is                                                                        | Default                                       |
| :------------- | :-------------------------------------------------------------------------------- | :-------------------------------------------- |
| `field`        | The signature field to sign. It must not be signed yet.                           |                                               |
| `kind`         | `'signature'`, or `'timestamp'` for a [document timestamp](#document-timestamps). | `'signature'`                                 |
| `signer`       | `{ name, reason, location, contactInfo, signedAt }`, written into the signature.  | none; `signedAt` now                          |
| `appearance`   | `{ pdf }`: a one-page PDF, drawn into the field's box.                            | none                                          |
| `certify`      | `{ permission: 1 \| 2 \| 3 }`: [certify](#certify-a-document) the document.       | not certified                                 |
| `lock`         | Fields this signature [locks](#lock-fields).                                      | none                                          |
| `digest`       | `'sha256'`, `'sha384'` or `'sha512'`.                                             | `'sha256'`, or the `key`'s hash with `sign()` |
| `subFilter`    | `'ETSI.CAdES.detached'` or `'adbe.pkcs7.detached'`.                               | `'ETSI.CAdES.detached'`                       |
| `contentsSize` | Bytes reserved for the signature, from 256 to 4 MiB.                              | `8192`                                        |

The field needs a widget to carry an `appearance`; without one, the call fails with
`SignatureRefused`. A PDF with more than one page fails with `InvalidArg`, as for a
[stamp](/docs/engine/annotations/stamps); take the page you want out first with
`doc.pages.extract()`.

## Sign in two steps

When the key is somewhere else (a signing service, a hardware key, another device), sign in two
steps. `prepare()` reserves room for the signature and returns the digest to sign; `complete()`
writes it in:

```ts
const prepared = await doc.signatures.prepare({
  field,
  signer: { name: 'Ada Lovelace', reason: 'Approved' },
});

// prepared.digest is the fingerprint to sign, with prepared.algorithm ('sha256').
const cms = await mySigningService.sign(prepared.digest, prepared.algorithm);

const result = await doc.signatures.complete({
  signingId: prepared.signingId,
  cms,
  expectedVersion: prepared.expectedVersion,
});
```

`cms` is a detached CMS signature (PKCS #7 or CAdES) over the digest: what signing services
return. To build one from a raw key yourself, use `buildDetachedCms()` from
`@embedpdf/core-signature`.

Between the two steps:

- **The document is locked.** Every other change fails with `SigningPending` until you complete or
  cancel; reading still works.
- **To give up**, call `doc.signatures.cancel(prepared.signingId)`. It returns
  `status: 'cancelled'`, and a `complete()` for that signing fails with `NotFound`.
- **Pass `expectedVersion` back as it is.** It ties the signature to the version it was made
  for; a different one fails with `SigningVersionMismatch`, and you prepare again.

`complete()` checks that `cms` is one signature that fits the space `prepare()` reserved
(`contentsSize`); if not, it fails with `SignatureRefused`. It doesn't check the cryptography:
`sign()` does that before it calls `complete()`, and `validateSignatures()` does it afterwards.
Calling `complete()` again with the same `cms` returns the same result, with
`status: 'already-completed'`; a different `cms` fails with `SignatureRefused`.

A signature can't be undone, and completing one ends undo for every change made before it; see
[Changes & undo](/docs/engine/documents/changes#what-cant-be-undone).

<Engine local>

A prepared signature waits until you complete or cancel it: `prepared.expiresAt` is `null`.

</Engine>

<Engine cloud>

A prepared signature waits 15 minutes for its CMS, until `prepared.expiresAt` (your server can
change the time). After that, `complete()` fails with `SigningExpired`.

</Engine>

## Certify a document

The first signature in a document can certify it: the signer says which changes may follow.

```ts
await sign(doc, { field, key, certify: { permission: 2 } });
```

| `permission` | What may change afterwards                                      |
| :----------- | :-------------------------------------------------------------- |
| `1`          | Nothing, except adding document timestamps and validation data. |
| `2`          | Filling in the form and signing.                                |
| `3`          | Filling in the form, signing and adding or changing comments.   |

From the moment it completes, the engine refuses what the certification forbids, with
`ProtectedDocument`: every level refuses page changes, new or changed fields, attachments and
redaction; `1` and `2` also refuse annotations; `1` also refuses filling in. Certifying after a
signature already exists fails with `SignatureRefused`.

## Lock fields

A signature can lock fields, so nobody changes what the signer saw:

```ts
await sign(doc, {
  field,
  key,
  lock: { action: 'include', fields: ['invoice.total', 'invoice.date'] },
});
```

`action` is `'include'` (lock the fields named), `'exclude'` (lock all others) or `'all'`. A name
covers the fields in its group: `'invoice'` locks `invoice.total` too. Once signed, writing a
locked field fails with `ProtectedDocument`, and an import skips it.

A signature field in a group (`groupId`, see [Signers and groups](/docs/engine/forms/groups))
locks the rest of its group when you name no `lock`: the group's other fields as they are when you
sign, but not its signature fields, which their signers still sign. Name a `lock` to choose
yourself.

## Signature fields and their look

A signature field is a form field of family `'signature'`. Create one like any field (see
[Creating form fields](/docs/engine/forms/authoring)):

```ts
const { field } = await doc.forms.create({
  family: 'signature',
  name: 'approver',
  widgets: [{ page: page.ref, rect: { x: 300, y: 682, width: 200, height: 60 } }],
});
```

To draw a mark into a field without signing it, for example a handwritten signature in a viewer
that has no key, use `setSignatureAppearance()`. The field stays unsigned:

```ts
await doc.forms.setSignatureAppearance(field.ref, { pdf: artworkPdf });
```

Once a field is signed, its look is part of what the signature seals, so
`setSignatureAppearance()` fails with `ProtectedDocument`. To sign with artwork, pass `appearance` to
`sign()` or `prepare()`.

<Engine cloud>

## On the cloud

- **Signing makes a new version.** A completed signature becomes a new version of the document on
  the server, and everyone who opens the document from then on gets the signed file.
- **Copies from before can't sign.** Someone who had the document open, and changed it, before the
  signature completed can keep editing their copy, but `prepare()` fails with `StaleBase`.
- **Signed bytes never change.** `getContents()`, `getDigest()` and `downloadRevision()` read a
  version, and a version is never changed, so the server lets browsers and CDNs cache them.

</Engine>

## Read the signatures

`list()` returns every signature field, signed or not, and what the signatures allow:

```ts
const { signatures, protection } = await doc.signatures.list();

for (const signature of signatures) {
  console.log(signature.fieldName, signature.signed, signature.signer.name);
}
```

| Field       | What it is                                                                                                                         |
| :---------- | :--------------------------------------------------------------------------------------------------------------------------------- |
| `field`     | The signature field's ref. Pass it to `sign()` and `prepare()`.                                                                    |
| `fieldName` | The field's full name.                                                                                                             |
| `signed`    | Whether the field holds a signature.                                                                                               |
| `signer`    | What the signer wrote: `name`, `reason`, `location`, `contactInfo` and `signedAt`.                                                 |
| `kind`      | `'signature'`, or `'timestamp'` for a document timestamp.                                                                          |
| `docMdp`    | The [certification](#certify-a-document) level the signature declares (`1`, `2` or `3`), or `null`.                                |
| `widget`    | The field's widget, with its `page` and `ref`, or `null` when the field has none. Where it shows is its row in `doc.forms.list()`. |

The `signer` fields are claims, the same ones you pass when you sign: anyone can write any name.
Who really signed is in the signature's certificate, which
[`validateSignatures()`](#check-the-signatures) checks. `signedAt` is the signer's own clock.

`protection` says what the signatures allow from now on; see
[Protection](#protection-what-signatures-allow).

The signatures describe the file as it was opened, or as it was last signed. Changes you haven't
saved aren't part of any signature until you sign.

## Check the signatures

`validateSignatures()` answers, for each signature: are the signed bytes intact, does the
signature match its certificate, do you trust that certificate, and what changed since.

```ts
import { validateSignatures } from '@embedpdf/core-signature';

const results = await validateSignatures(doc, {
  trust: { anchors: async () => [rootCertificate] },
});

for (const result of results) {
  console.log(result.signature.fieldName, result.summary);
}
```

`anchors` returns the certificates you trust (DER bytes), for example your company's root.

| Field                   | What it answers                                | Values                                                         |
| :---------------------- | :--------------------------------------------- | :------------------------------------------------------------- |
| `integrity`             | Are the signed bytes unchanged?                | `'valid'`, `'invalid'`, `'indeterminate'`                      |
| `cryptography`          | Does the signature match its own certificate?  | `'valid'`, `'invalid'`, `'unsupported'`                        |
| `trust`                 | Does the certificate lead to one you trust?    | `'trusted'`, `'untrusted'`, `'unknown'`                        |
| `modifications.verdict` | What changed after signing, and is it allowed? | See [What changed](#what-changed-since-a-signature)            |
| `summary`               | All of the above together.                     | `'valid'`, `'valid-untrusted'`, `'invalid'`, `'indeterminate'` |

`summary` is `'invalid'` only on proof: bytes that changed, a signature that doesn't match, or a
change the signature forbids. When something can't be established, it's `'indeterminate'`.
Without `trust`, a good signature is `'valid-untrusted'` at best.

By default the check looks at the saved file. Pass `until: 'working-copy'` to include the changes
you haven't saved yet: the answer is what the file will say once you save it. A viewer uses this
to warn before a change breaks a signature.

## What changed since a signature

`analyze()` compares the document with the version a signature sealed, and says whether what
changed is allowed:

```ts
const analysis = await doc.signatures.analyze({
  since: { signatureIndex: 0 },
  until: 'working-copy',
});

console.log(analysis.verdict);
```

| Verdict           | Meaning                                                                                         |
| :---------------- | :---------------------------------------------------------------------------------------------- |
| `'unchanged'`     | Nothing changed since the signature.                                                            |
| `'permitted'`     | Something changed, and the signature allows it, such as a field filled in or another signature. |
| `'forbidden'`     | A change the signature doesn't allow, such as a rotated page.                                   |
| `'indeterminate'` | The engine can't tell: part of the file can't be read, or its structure is broken.              |

With `'forbidden'`, the signature itself still checks out, but PDF apps such as Acrobat report
that the document was changed after signing. For what `since` and `until` take, see
[Analyzing in detail](#analyzing-in-detail).

## Events

| Event                  | Fired by                                                                                       |
| :--------------------- | :--------------------------------------------------------------------------------------------- |
| `signatures.prepared`  | `prepare()`, with the `signingId` and the `field`.                                             |
| `signatures.completed` | `complete()`, with what it returned.                                                           |
| `signatures.cancelled` | `cancel()`.                                                                                    |
| `document.versioned`   | `complete()`: the document is now the signed file. Read revisions, digests and verdicts again. |

`sign()` fires the same events. See [Events](/docs/engine/concepts/events).

<Engine cloud>

Everyone else with the document open receives the `signatures.*` events too, with
`origin.kind: 'remote'`, so their viewer can show the new signature.

</Engine>

## Permissions

Reading signatures (`list()`, `analyze()`, `getContents()`, `getDigest()`) needs
`doc.forms.read`; `downloadRevision()` needs `doc.download`. Signing (`prepare()`, `complete()`,
`cancel()`) needs `doc.sign`, or `fields:sign` for the field's group, and certifying also
`doc.sign.certify`. See [Permissions](/docs/engine/concepts/permissions).

A call the document's own signatures forbid fails with `ProtectedDocument`, whatever the user's
permissions. A field that can't be signed (already signed, read-only, or locked by an earlier
signature) fails with `SignatureRefused`; the message says why.

## Good to know

### Protection: what signatures allow

`protection` from `list()` answers two different questions:

| Field           | Question                                       | Values                                              |
| :-------------- | :--------------------------------------------- | :-------------------------------------------------- |
| `enforced`      | What does the engine refuse?                   | A level, or `null` when no signer declared a limit. |
| `judged`        | What keeps the signatures valid?               | A level, or `null` when nothing is signed.          |
| `certification` | The certifying signature and its `permission`. | `null` when the document isn't certified.           |
| `fieldLocks`    | The fields signatures locked.                  | A list, empty when no field is locked.              |

A level is `'lta'` (only document timestamps and validation data), `'fill'` (filling in and
signing) or `'annotate'` (also comments).

`enforced` is what the engine refuses. Only a certification or a field lock sets it; the calls it
refuses fail with `ProtectedDocument`. A plain approval signature sets nothing, so every change
stays possible.

`judged` is what keeps signatures valid in Acrobat. For an approval signature it's `'annotate'`:
filling in, signing and commenting keep it valid, while a rotated page, for example, makes
Acrobat report that the document was changed.

Use `allows()` to hide what the session may not do; a document's signatures can still refuse a
change, with `ProtectedDocument`:

```ts
if (!doc.security.allows('doc.pages.assemble')) hidePageTools();
```

One save is always refused on a signed document: a rewrite (`download({ mode: 'rewrite' })`)
would remove every signature, so it fails with `ProtectedDocument`. The default incremental save
keeps them; see [Saving](/docs/engine/documents/saving).

### Analyzing in detail

`since` takes the signature's position in `list()` (`{ signatureIndex }`), or a revision (see
[Revisions and byte ranges](#revisions-and-byte-ranges)). `until` is `'persisted'` (the saved file,
the default), `'working-copy'` (with your unsaved changes) or `{ revisionIndex }`.

`analysis.current.primary` is the change that decided the verdict, with the rule that allowed or
forbade it. Show it next to a `'forbidden'` verdict.

### Revisions and byte ranges

A PDF that keeps its signatures grows by appending: each save adds a **revision** at the end of
the file, and the older bytes stay as they were. A signature covers every byte of the revision it
was made in, except the hole it sits in (the signature dictionary's `/ByteRange` and `/Contents`).

`list()` also returns `revisions`, oldest first, each with its `index`, its `end` (the byte length
of the file at that revision) and the `signatureIndex` that seals it. Each signature has its
`revisionIndex` and a `coverage`:

- `'whole-revision'`: it covers exactly one revision. That's the normal case.
- `'partial'`: its byte ranges are valid but seal no revision. It can't be checked against later
  changes, so `analyze()` from it fails with `InvalidArg`.
- `'malformed'`: its byte ranges or its signature bytes can't be used.

`chainValid: false` means the file's cross-reference chain is broken or was rebuilt, so no fact
about its bytes can be trusted.

### The raw bytes

For your own verifier: `getContents(field)` returns the signature (DER, without padding),
`getDigest(field, algorithm)` hashes the bytes the signature covers (`'sha1'` works too, for old
signatures), and `downloadRevision(index)` returns the file exactly as it was at a revision.

### How changes are judged

- A signature is judged on the document as it is now against the revision it sealed. A change
  that was undone later doesn't count, and an object written again with the same value isn't a
  change.
- A certification also checks every revision in between, so a forbidden change stays forbidden
  even when a later revision hides it.
- Missing evidence is never a pass: a value the engine can't read makes the verdict
  `'indeterminate'`.
- The rules follow what Acrobat reports.
- `exploratoryLevel` asks what a level would allow; the answer (`mode: 'exploratory'`) isn't a
  verdict.
- `detail: 'full'` lists every changed object (large; for tools).

### Unsaved changes when you sign

Your unsaved changes become part of the signed version, so the signature seals them too. You
don't need to save before you sign.

### Document timestamps

`kind: 'timestamp'` makes a document timestamp instead of a signature. With `sign()`, the `key` is
your timestamp authority: a `remoteSigner` whose `sign` sends the digest to the authority and
returns its timestamp token (`subFilter` is `'ETSI.RFC3161'`). A key you hold can't make one, so
`sign()` fails with `InvalidArg`. With `prepare()` and `complete()`, the `cms` is the token over
`prepared.digest`.

```ts
const result = await sign(doc, { field, kind: 'timestamp', key: myTimestampAuthority });
result.signature.kind; // 'timestamp'
```

<Engine local>

### Turning protection off

For tools that must produce or test invalid files, create the engine with
`localEngine({ signedDocumentPolicy: 'permit' })`: the engine then refuses nothing a signature
forbids. `judged` and every verdict stay the same.

</Engine>

---

### Content Engine Text Extraction (`docs/content/engine/text/extraction.mdx`)

---
title: Reading text
description: Get the text of a page, part of a page, or a whole document.
---

import { Callout } from '@/components/docs/callout';

# Reading text

Get a page's text with `page.text.get()`:

```ts
const page = doc.page(pageRef);
const { text } = await page.text.get();
```

`text` is the page's text in reading order, as a normal JavaScript string. Lines are separated
by `\r\n`.

## Part of a page

Characters on a page are numbered from 0. To read just some of them (the text a user selected, or
a search match), pass a range:

```ts
const selected = await page.text.slice({ start: 120, count: 42 });
```

A range is always `{ start, count }`: `start` is the first character, `count` how many. Search
matches and selections are ranges too, so you can pass them straight in:

```ts
const { matches } = await doc.search.query({ text: 'total' });
const found = await doc.page(matches[0].page).text.slice(matches[0]);
```

## A whole document

There's no single call for the whole document; read the pages and join them:

```ts
const { pages } = await doc.pages.list();

const texts = await Promise.all(pages.map((page) => doc.page(page.ref).text.get()));
const fullText = texts.map(({ text }) => text).join('\n\n');
```

The engine reads pages one at a time in the background, so this is fine for long documents too.

## Permissions

Reading text needs the `doc.text.copy` permission, because text you read can leave the document.
Without it, both calls fail with `Forbidden`.

A user without `doc.text.copy` can still [search](/docs/engine/text/search) (without snippets)
and [select](/docs/engine/text/selection-and-geometry) text: both need only where the text is,
not what it says. That lets you allow highlighting a PDF while stopping its text from being
copied.

<Engine local>

<CloudPdfCallout
  title="Stop text from being copied, for real"
  href="https://www.cloudpdf.com/docs/engine/concepts/permissions"
  cta="How CloudPDF enforces permissions"
>

The local engine runs in the user's browser, which already holds the whole file, so a permission
there keeps your interface honest but can't keep the text from a user who opens the file another
way. With CloudPDF the file stays on your server: a user without `doc.text.copy` receives page
images and highlight positions, never the text as characters they can copy.

</CloudPdfCallout>

</Engine>

## Good to know

### Characters and string positions

The engine numbers characters the way the PDF stores them. Most of the time that matches the
string: character 5 is `text[5]`. Two things make them differ:

- **Characters outside the Basic Multilingual Plane** (most emoji, some CJK characters) are one
  character in the PDF but two positions in a JavaScript string.
- **Characters with no text of their own** (a glyph the PDF maps to no Unicode character) have a
  number but add nothing to the string.

That's why you pass ranges to `slice()` instead of cutting `text` yourself: `slice()` converts for
you. `get()` also returns `charCount` (how many characters the page has) and, when the two
numberings differ, a `charMap` that records where. You only need `charMap` if you convert
positions yourself, for example to run your own search over `text`:

```ts
import { charRangeForTextOffsets } from '@embedpdf/engine';

const snapshot = await page.text.get();
const at = snapshot.text.indexOf('Total');
const range = charRangeForTextOffsets(snapshot, { start: at, count: 'Total'.length });
// range is { start, count } in characters: ready for slice(), layout.segments() or a highlight.
```

<Engine cloud>

### Caching

The text of a page only changes when the page's content changes (not when annotations do). The
server caches it with the page, so reading an unchanged page again is fast.

</Engine>

---

### Content Engine Text Search (`docs/content/engine/text/search.mdx`)

---
title: Search
description: Find text in a document, show where each match is, and page through results in a large file.
---

import { Callout } from '@/components/docs/callout';

# Search

`doc.search.query()` finds text in the whole document:

```ts
const { matches } = await doc.search.query({ text: 'invoice' });

for (const match of matches) {
  console.log(match.page, match.start, match.count);
}
```

Each match tells you three things:

- `page`: the page it's on (a page ref, like `page.ref` from `doc.pages.list()`).
- `start` and `count`: which characters it covers, as a range on that page.
- `segments`: where it is on the page, one segment per line the match covers.

## Show where a match is

Each segment has a `rect` (a box, `{ x, y, width, height }` from the page's top-left) and a `quad` (the
four corners of the text, which differ from the box when text is rotated). Draw the boxes over
the rendered page, or turn the match into a highlight annotation:

```ts
await doc.page(match.page).annotations.create({
  subtype: 'highlight',
  quadPoints: match.segments.map((segment) => segment.quad),
});
```

Segments are the same ones a text selection makes, so a highlighted match looks exactly like a
highlighted selection of the same words. A match is also a range, so the selection plugin and
`page.text.slice()` take it as it is. For the other markup kinds and their fields, see
[Highlights & text markup](/docs/engine/annotations/text-markup).

## Show the text around a match

Ask for snippets to show each match in context, for a results list:

```ts
const { matches } = await doc.search.query({ text: 'invoice', snippets: true });

const { before, match, after } = matches[0].snippet!;
// before: "Please pay this "   match: "invoice"   after: " within 30 days."
```

Render `match` in bold or with a `<mark>` between `before` and `after`.

## Options

```ts
await doc.search.query({ text: 'Total', matchCase: true, wholeWord: true });
```

| Option             | What it does                                                                 | Default    |
| :----------------- | :--------------------------------------------------------------------------- | :--------- |
| `text`             | What to find.                                                                |            |
| `matchCase`        | `Total` doesn't match `total`.                                               | `false`    |
| `wholeWord`        | `pay` doesn't match `payment`.                                               | `false`    |
| `matchDiacritics`  | `café` doesn't match `cafe`.                                                 | `false`    |
| `ignoreWhitespace` | `invoice` also matches `i n v o i c e` (letter-spaced headings, OCR).        | `false`    |
| `regex`            | `text` is a regular expression.                                              | `false`    |
| `snippets`         | Include the text around each match.                                          | `false`    |
| `from`             | Start at this page and wrap around, so the matches the user sees come first. | first page |

By default the search ignores case and accents, and treats any run of spaces and line breaks as
one space, so `total amount` finds a phrase broken over two lines.

### Regular expressions

With `regex: true`, `text` is a pattern in JavaScript syntax without backreferences and
lookaround (so it runs the same on every engine). `matchCase` and `wholeWord` still apply;
`matchDiacritics` and `ignoreWhitespace` don't, and passing them with `regex` fails with
`InvalidArg`, as does a pattern the engine can't run.

```ts
await doc.search.query({ text: 'INV-\\d{4}', regex: true });
```

To check a query before sending it (for example while the user types), use
`validateSearchQuery(query)`. It runs the same check as the engine and returns `{ ok: true }`, or
`{ ok: false, message }` with a message you can show:

```ts
import { validateSearchQuery } from '@embedpdf/engine';

const check = validateSearchQuery({ text: '(unclosed', regex: true });
if (!check.ok) showError(check.message);
```

## Large documents

A search returns matches in batches, so the first results show up quickly even in a very long
document. When there's more to find, the result has a `nextCursor`; pass it back to continue:

```ts
let cursor: string | null = null;

do {
  const result = await doc.search.query({ text: 'invoice', cursor: cursor ?? undefined });
  showMatches(result.matches);
  showProgress(result.pagesSearched, result.pageCount);
  cursor = result.nextCursor;
} while (cursor);
```

`nextCursor` is `null` when the whole document has been searched. To stop a search (the user
typed something else), just don't ask for the next batch.

A cursor belongs to one query on one version of the document. If the document changes between
batches, the next call fails with `InvalidArg`: start the search again.

To choose how much each batch does, pass `limit: { matches, pages }`. The engine may do less than
you ask.

## Permissions

Searching needs the `doc.text.search` permission. Snippets are text from the document, so
`snippets: true` also needs `doc.text.copy`; without it the call fails with `Forbidden` rather
than leaving the snippets out.

That means you can let a user find words in a document (and see where they are) without letting
them read its text, which is often what a document owner wants.

<Engine local>

<CloudPdfCallout
  title="Search without sending the text"
  href="https://www.cloudpdf.com/docs/engine/concepts/permissions"
  cta="How CloudPDF enforces permissions"
>

On the local engine the search runs in the browser, over a file the user already has. With
CloudPDF it runs on your server, and a user without `doc.text.copy` gets where the matches are,
not the text around them.

</CloudPdfCallout>

</Engine>

---

### Content Engine Text Selection And Geometry (`docs/content/engine/text/selection-and-geometry.mdx`)

---
title: Selection & geometry
description: Find the character under the pointer, select words and lines, and get the boxes to draw a selection or a highlight.
---

import { Callout } from '@/components/docs/callout';

# Selection & geometry

A PDF page doesn't store sentences; it stores characters and where each one is drawn. The
engine gives you that layout, so you can find the text under the pointer, select a word or a
line, and draw the selection yourself.

```ts
const layout = await page.text.layout();

const word = layout.wordAt({ x: 120, y: 90 }); // { start, count } or null
const boxes = word ? layout.segments(word) : []; // where to draw it
```

<Callout type="info">
  Using the viewer or the headless components? The [Selection
  plugin](/docs/headless/text/selection) does all of this for you, including drag and touch
  handles. This page is for building your own.
</Callout>

## The layout

`page.text.layout()` reads where every character on the page is. Read it once per page and keep
it: every method on it answers right away, without another call to the engine.

```ts
const layout = await page.text.layout();
```

Positions are in page space: points (1/72 inch) from the top-left corner of the page, with `y`
going down, the way every position in the engine is.

## Find the character under the pointer

`charAt(point)` returns the number of the character at a point, or `null` when there's no text
there. It forgives a near miss, so a click just beside a letter still finds it.

```ts
const index = layout.charAt(point);
```

To turn a click on a rendered page into a point, use the image's `transform`. It knows the page's
rotation and your zoom:

```ts
const image = await page.render.image({ viewport: { kind: 'width', width: 800 } });

img.addEventListener('click', (event) => {
  const scale = image.width / img.clientWidth; // image pixels per CSS pixel
  const point = image.transform.pixelsToPage({
    x: event.offsetX * scale,
    y: event.offsetY * scale,
  });
  const index = layout.charAt(point);
});
```

`scale` turns the click's CSS pixels into image pixels, so this works at whatever size you show
the image. Without an image at hand (while zooming, or for a page you draw yourself),
`pageTransform()` gives the same transform from the page's layout; see
[From image pixels to page points](/docs/engine/documents/rendering#from-image-pixels-to-page-points).

## Select a word, a line, a range

Double-click and triple-click, the way text editors do it:

```ts
const word = layout.wordAt(point); // the word under the point
const line = layout.lineAt(point); // the whole line
```

Both return a range, `{ start, count }`, or `null`. When you already have a character number,
pass it instead of a point: `layout.wordAt(42)`. For a drag, take the characters at the start and
at the end of the drag and select everything between them:

```ts
const from = layout.charAt(dragStart);
const to = layout.charAt(dragEnd);

if (from !== null && to !== null) {
  const range = { start: Math.min(from, to), count: Math.abs(to - from) + 1 };
}
```

## Draw the selection

`segments(range)` returns one segment per line the range covers:

```ts
for (const segment of layout.segments(range)) {
  const box = image.transform.pageToPixels(segment.rect); // { x, y, width, height } in image pixels
  drawSelectionBox(box);
}
```

Each segment has:

| Field  | What it is                                                                                                                                        |
| :----- | :------------------------------------------------------------------------------------------------------------------------------------------------ |
| `rect` | The box around the text on that line, `{ x, y, width, height }`.                                                                                  |
| `quad` | The four corners of the text, `{ upperLeft, upperRight, lowerLeft, lowerRight }`, named as the text stands. Differs from `rect` for rotated text. |

Use `rect` for a simple box. Use `quad` when text can be rotated, and for annotations: a highlight
takes quads.

## Copy it, or highlight it

A range works everywhere a range goes. Read its text with `slice()`, or make it a highlight:

```ts
const text = await page.text.slice(range);

await page.annotations.create({
  subtype: 'highlight',
  quadPoints: layout.segments(range).map((segment) => segment.quad),
});
```

Search matches are ranges too, with their segments already worked out: see
[Search](/docs/engine/text/search). For underlines, strikeouts and the highlight's fields, see
[Highlights & text markup](/docs/engine/annotations/text-markup).

## Permissions

The layout needs the `doc.text.select` permission: it tells you where text is, not what it says.
`slice()` needs `doc.text.copy`. So a user may select and highlight text without being able to
copy it.

## Good to know

### Lines, not letters

Segments follow visual lines: a selection over three lines is three segments, however many
letters or fonts it covers. Selection, search and highlights all use the same segments, so they
line up exactly.

### Rotated and vertical text

On a line that runs at an angle, `rect` is the upright box around it and `quad` follows the text.
The corners of a quad are, in order: the two ends of the top edge, then the two ends of the bottom
edge, each from the line's start side (the left, for upright text). That's the order PDF
highlights use, so you can pass quads to an annotation unchanged.

The corners describe the shape, not the reading direction. For that, each segment has `advance`:
`1` when the text reads from the start side (left to right on an upright line), `-1` when it reads
the other way, as Arabic and Hebrew do.

### The raw geometry

`layout.runs` is the raw geometry: characters grouped by line and font. Each character has a
loose box (the full line height; segments use it) and, when the font says, a tight box around the
glyph (`charAt` checks it first). A space has `space: true`, and a character with no box of its
own has `empty: true` and a zeroed box. You need runs only for things like one box per letter.

Two shortcuts save you reading runs: `layout.charCount` is how many characters the page has, and
`layout.charQuad(index)` is one character's box as a quad, or `null` when it has none (use it to
place a caret at the edge of a selection).

A run of turned text also has `rotation`: how far its text is turned from upright, in degrees
clockwise as the page shows it, from 0 up to 360, like every other `rotation` in the engine. Text
that reads straight up the page is 270; text that reads down it is 90.

---

### Content Headless Annotations Comments (`docs/content/headless/annotations/comments.mdx`)

---
title: Comments & replies
description: Build a comments sidebar with threads, replies, review states and jumping to a comment.
---

# Comments & replies

Any annotation can carry a comment, its `contents`, and people can reply to it. Together they
make a **thread**: the first comment and its replies, in order. `useCommentThreads()` gives you
every thread in the document, ready for a sidebar:

<Snippet name="annotations/comments/sidebar" />

<Example name="annotation/comments" />

> **Try it:** reply to a thread, or press **Show** to go to its comment. Add a note on the page:
> it gets a thread of its own.

Threads come in reading order: by page, then from the top of the page. The list updates by itself
when anyone adds, changes or deletes a comment, on this screen or, on a shared document, on
someone else's.

A thread has:

| Field       | What it is                                                                                        |
| :---------- | :------------------------------------------------------------------------------------------------ |
| `root`      | The annotation that starts the thread: a note, a highlight, a shape.                              |
| `replies`   | Every reply, oldest first. Replies to replies are included.                                       |
| `review`    | The review states people set, such as accepted or rejected (see [Review states](#review-states)). |
| `page`      | The page it's on.                                                                                 |
| `pageLabel` | The page's label to show, such as `'iv'` or `'7'`.                                                |

## Reply, edit and delete

```ts
const { annotation: reply } = await comments.reply(thread.root.ref, 'Checked, looks right.');
await comments.setText(reply.ref, 'Checked against the invoice, looks right.');
await comments.delete(reply.ref); // one comment
await comments.deleteThread(thread.root.ref); // the whole thread
```

A reply always goes to the thread's first comment, whichever comment you pass. It's saved the way
Acrobat saves replies, so they show up as replies there too.

Deleting the first comment of a thread deletes the whole thread, as Acrobat does. `deleteThread()`
does the same, all or nothing, and tells you what happened: when the thread has a comment you may
not delete, such as someone else's reply, nothing is deleted and `failed` lists what stopped it.

## Review states

A review state is a verdict on a thread: accepted, rejected, cancelled or completed. Each person
sets their own, and a check mark is separate:

```ts
await comments.setStatus(thread.root.ref, 'accepted');
await comments.setMarked(thread.root.ref, true); // check it off

thread.review.lastChange?.state; // the latest verdict, from anyone
thread.review.byReviewer; // each person's latest verdict
thread.review.markedBy; // who has checked it off
```

States are saved as the PDF's own review states, so Acrobat shows them in its comment list.

<Example name="annotation/review" />

## Jump to a comment

The thread's `root` has a page and a `rect`, which is where the view should go:

<Snippet name="annotations/comments/jump" />

## Methods

Everything `useComments()` gives you. It's the `comments` part of `useAnnotation()`.

| Method                   | What it does                                                                                                                                         |
| :----------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- |
| `listThreads()`          | Every thread, in reading order. In a component, use `useCommentThreads()`, which also gives each thread its `pageLabel`.                             |
| `getThread(ref)`         | The thread a comment is in, whether you pass its first comment, a reply or a review state, or `null`. In a component, use `useCommentThread(ref)`.   |
| `reply(ref, text)`       | Replies to the thread's first comment, whichever comment you pass. Resolves `{ annotation }`. [Reply, edit and delete](#reply-edit-and-delete)       |
| `setText(ref, text)`     | Changes a comment's text                                                                                                                             |
| `delete(ref)`            | Deletes a comment with its replies. The first comment of a thread deletes the whole thread.                                                          |
| `deleteThread(ref)`      | Deletes a whole thread, all or nothing. Resolves `{ deleted, failed }`: when a comment in it may not be deleted, nothing goes and `failed` lists it. |
| `setStatus(ref, state)`  | Sets the user's review state on a thread, such as `'accepted'` or `'rejected'`. [Review states](#review-states)                                      |
| `setMarked(ref, marked)` | Checks a thread off for the user, or clears the check mark                                                                                           |
| `canReply(ref)`, `canSetText(ref)`, `canDelete(ref)`, `canDeleteThread(ref)`, `canSetStatus(ref)`, `canSetMarked(ref)` | Whether the user may do that to this comment. [Permissions](#permissions)                                                                            |

Every method that returns a promise also takes a `signal` in its options, to cancel it.

## Events

<Snippet name="annotations/comments/events" />

| Event                      | Fires when                                         | Carries              |
| :------------------------- | :------------------------------------------------- | :------------------- |
| `comments.onThreadChanged` | A thread changed through this viewer               | `rootRef`, `change`  |

`change` is `'reply'`, `'text'`, `'status'`, `'marked'` or `'deleted'`. For every change, including
other people's, use the annotation events; see [Events, saving & sharing](saving#events).

## Permissions

Every comment verb has a check that answers for one comment, so you show only the buttons that work:

<Snippet name="annotations/comments/permissions" />

| Permission           | What it allows                                                     | Check                                     |
| :------------------- | :----------------------------------------------------------------- | :---------------------------------------- |
| `annotations:create` | Replying, setting a review state, checking a comment off            | `canReply(ref)`, `canSetStatus(ref)`, `canSetMarked(ref)` |
| `annotations:update` | Changing a comment's text                                          | `canSetText(ref)`                         |
| `annotations:delete` | Deleting a comment, or every comment in a thread                   | `canDelete(ref)`, `canDeleteThread(ref)`  |

A review state and a check mark are small replies of their own, the way Acrobat stores them, so they
need the right to create, not to change. The checks also follow the comment's own flags:
`lockedContents` stops text edits, and `locked` stops deleting.

## Good to know

### Comments without a note

Any annotation can start a thread. A highlight with a comment is a thread whose `root` is the
highlight. A plain note (the `note` tool) is a thread with an icon on the page.

### Loading

<Fw only={['react', 'vue', 'svelte']}>The `status` from `useAnnotationState()`</Fw><Fw only="angular">The
`status()` signal on `inject(EpdfAnnotation)`</Fw> is `'loading'` until every annotation is in. Show a
loading state in your sidebar until then, so an empty list doesn't look like "no comments".

### Reviewing together

<Engine local>

On the local engine, a review stays with the file: the next person sees the comments once they have
the downloaded PDF.

<CloudPdfCallout
  title="Turn comments into a live review"
  href="https://www.cloudpdf.com/docs/headless/annotations/comments"
  cta="Review with CloudPDF"
>

With CloudPDF, everyone with the document open sees new comments, replies and review states as they
happen, and the server checks who may reply, change or delete what. Your sidebar stays the same:
`useCommentThreads()` updates by itself.

</CloudPdfCallout>

</Engine>

<Engine cloud>

Everyone with the document open sees new comments, replies and review states as they happen:
`useCommentThreads()` updates by itself.

</Engine>

---

### Content Headless Annotations Custom Rendering (`docs/content/headless/annotations/custom-rendering.mdx`)

---
title: Your own annotation look
description: Draw annotations with your own components, react to hover, show cards next to them, and make them interactive.
---

# Your own annotation look

The annotation layer draws each annotation the way the PDF describes it. You can draw some of them
with your own <Fw only={['react', 'vue', 'svelte']}>component</Fw><Fw only="angular">template</Fw> instead: a note that looks like your app's comment bubble, a stamp
that shows live data from your server.

This changes the screen only. The PDF keeps the annotation's own look, so other PDF apps,
printing and downloads show the original:

| Where                               | What people see                 |
| :---------------------------------- | :------------------------------ |
| Your viewer                         | Your <Fw only={['react', 'vue', 'svelte']}>component</Fw><Fw only="angular">template</Fw> |
| The saved PDF, other apps, printing | The annotation's own appearance |

## Its look, or UI on it

Ask whether it should zoom and turn with the PDF:

| You want | Use | It |
| :-- | :-- | :-- |
| A different look: a comment bubble instead of the note icon, a stamp with live data | A [renderer](#draw-an-annotation-your-way) | Is part of the page: it zooms and turns with it, and sits under the selection handles |
| A mark that stays on the annotation: a ✓, a status, a count of replies | [Pinned anchored UI](#pin-ui-to-an-annotation) | Floats over the pages, upright and the same size at every zoom, exactly where you put it |
| A card or a menu next to it | [Anchored UI](#show-a-card-on-hover) | Floats over the pages, and moves to stay in view |

## Draw an annotation your way

<Fw only={['react', 'vue', 'svelte']}>Give the layer a list of **renderers**. Each one says which
annotations it draws and with which component:</Fw><Fw only="angular">Put a **renderer** in the layer for
each look: an `<ng-template epdfAnnotation>` that says which annotations it draws, and draws them:</Fw>

<Snippet name="annotations/custom-rendering/renderers" />

<Fw only={['react', 'vue', 'svelte']}>`for`</Fw><Fw only="angular">The function you give `epdfAnnotation`</Fw>
gets the annotation, with all its fields, so you can pick by kind, name, author or anything else.<Fw only="angular">
A subtype picks every annotation of that kind: `epdfAnnotation="stamp"`.</Fw> The first renderer that
matches wins; an annotation no renderer matches is drawn as usual.

<Fw only="react">

Define the list once, outside your component or in `useMemo`. The layer registers each entry, and
a new list every render would register them again (the viewer warns about it while you develop).

</Fw>

Your <Fw only={['react', 'vue', 'svelte']}>component</Fw><Fw only="angular">template</Fw> draws into a **frame**: a box the layer places, turns and scales exactly like
the annotation's own drawing, also while it's dragged, resized or turned. Fill it, with `width` and
`height` at `100%`, and draw at the annotation's 100% size: the layer scales your drawing with the
page, text and borders included, as it does the PDF's own:

<Snippet name="annotations/custom-rendering/bubble" />

The selection outline, the click area and the hover all use the frame, so a look that fills it
matches them. A note keeps its size on screen as people zoom in and stays upright when the page is
turned, and so does a look that fills its frame. To make the bubble bigger, make the note bigger: a
size in points is a third more in pixels at 100% zoom, so a 24-point note is 32 pixels.

<Fw only={['react', 'vue', 'svelte']}>Your component gets:</Fw><Fw only="angular">Your template gets these:
the annotation is `let-annotation`, and the rest are named, such as `let-frame="frame"`.</Fw>

| Prop          | What it is                                                                          |
| :------------ | :---------------------------------------------------------------------------------- |
| `annotation`  | The annotation, as `get()` returns it.                                              |
| `frame`       | The box you draw into: its `width` and `height` in pixels at the annotation's 100% size, its `rotation` on screen in degrees clockwise, and the `scale` the layer draws it at. Turn your content by `-rotation` to keep it upright, such as a label on a turned stamp. |
| `native`      | The layer's own drawing of it, filling the frame. Render it to keep the original look and add to it. |
| `appearance`  | The engine's picture of the annotation as an image (`url`), or `null`. `native` already draws it in place. |
| `hovered`     | True while the pointer is over it.                                                  |
| `selected`    | True while it's selected, such as to make room for the selection handles.           |
| `interactive` | True when what you draw takes the pointer (see [below](#take-over-the-pointer)).    |

<Example name="annotation/renderers" />

> **Try it:** the notes are bubbles with their author's initials. Click one: the selection fits it.
> Press **Turn the view**: they stay upright, like the PDF's own notes. Switch to **The PDF's look**:
> that's what the saved PDF shows.

People can still select, move, resize and delete the annotation: your <Fw only={['react', 'vue', 'svelte']}>component</Fw><Fw only="angular">template</Fw> draws, and the
viewer keeps handling the pointer. That's also why it gets no clicks of its own. For that, see
[Take over the pointer](#take-over-the-pointer).

## Show a card on hover

A hover card sits next to an annotation while the pointer is over it. It needs two things: the
annotation under the pointer, and an anchor that keeps a card attached to it:

<Snippet name="annotations/custom-rendering/hover-card" />

<Example name="annotation/hover-card" />

<Fw only="react">Mount it in the Stage's `overlay`</Fw><Fw only="vue">Mount it in the Stage's `#overlay`
slot</Fw><Fw only="svelte">Mount it in the Stage's `overlay` snippet</Fw><Fw only="angular">Put it inside
`<epdf-stage>`</Fw>, like a menu. The card stays next to the annotation while the user scrolls and
zooms, and while the annotation is moved.

`useAnnotationAnchor(ref)` works for any annotation, not only the hovered one: see the next section.

## Pin UI to an annotation

A ✓ on an approved stamp, a status under it, a count of replies next to a note: UI that belongs to
one annotation and stays on it. `pinned` keeps it exactly where you put it. It doesn't flip or move to
stay in view, and it scrolls away with the annotation:

<Snippet name="annotations/custom-rendering/pinned" />

<Fw only="react">Mount one for each annotation that needs it in the Stage's `overlay`</Fw><Fw only="vue">Mount one for each annotation that needs it in the Stage's `#overlay` slot</Fw><Fw only="svelte">Mount one for each annotation that needs it in the Stage's `overlay` snippet</Fw><Fw only="angular">Put one for each annotation that needs it inside `<epdf-stage>`</Fw>,
from `useAnnotationList()`. Anchored UI on a page that isn't on screen isn't drawn, so a document
with hundreds of them costs only the ones in view.

<Example name="annotation/stamp-status" />

> **Try it:** sign the second stamp off, then select a stamp and move it: its status goes with it. The
> second stamp's status hangs over the next page, and stays on top.

1. The stamps are placed like any stamp, so the PDF holds the real ones; see [Stamps](stamps).
2. Each one is picked out by its `name`, which the stamp keeps in every PDF app.
3. The status sits under the stamp with `placement="bottom"`, and the ✓ on its corner with
   `placement="top-end"` and a negative `gap`. See [Where it goes](../ui/anchored#where-it-goes).
4. The stamp keeps its own look and stays selectable: the buttons are next to it, not in it.

To find your own data for an annotation, key it by `annotationKey(annotation.ref)`, which stays the
same after the document is saved and opened again, or by its `name` when you gave it one.

## Take over the pointer

Sometimes your <Fw only={['react', 'vue', 'svelte']}>component</Fw><Fw only="angular">template</Fw> must take clicks itself: buttons, links, a text field. <Fw only={['react', 'vue', 'svelte']}>Mark the
renderer `interactive`:</Fw><Fw only="angular">Add `epdfAnnotationInteractive` to the renderer:</Fw>

<Snippet name="annotations/custom-rendering/interactive" />

This is for when the annotation itself is the control, such as a stamp you tick like a checkbox. For
buttons next to an annotation, [pin them](#pin-ui-to-an-annotation) instead: the annotation stays
selectable.

While a renderer is interactive, the pointer belongs to your <Fw only={['react', 'vue', 'svelte']}>component</Fw><Fw only="angular">template</Fw>, and people can't select,
move or delete that annotation with it. Most apps want both: interactive while reading, editable
while editing. Pass a function, and the viewer asks it whenever it matters:

<Snippet name="annotations/custom-rendering/interactive-when" />

## Good to know

### Drawing at the size on screen

A renderer with `scale: false` draws at the frame's size on screen instead, and sizes its text and
borders itself from `frame.scale`. Use it for something that must lay itself out at full resolution,
such as a canvas.

### Hover cards people can use

A card closes when the pointer leaves the annotation, so the pointer can't reach a button in it.
Keep the card open while the pointer is over the card itself, for example with a short delay
before it closes and a `pointerenter` on the card that cancels it.

### Form fields

A form field's widgets aren't annotations: they're the form's, and the `<FormLayer>` draws them, so
your renderers never get them. See [Forms and annotations](../forms#forms-and-annotations).

### Other layers

A renderer replaces the drawing inside the annotation layer. Anything that isn't tied to one
annotation, such as a comments sidebar, belongs in your own UI, fed by `useAnnotationList()`.

---

### Content Headless Annotations Measurements (`docs/content/headless/annotations/measurements.mdx`)

---
title: Measurements
description: Measure distances, perimeters and areas on drawings and plans at a real-world scale, and calibrate a page from a known length.
---

# Measurements

On a floor plan or a technical drawing, people want real sizes: how long this wall is, how big this
room. The measurement plugin adds the tools for it, and keeps each page's scale, so a line of 3
centimeters on the page reads as 3 meters in the room. Register `measurementPlugin()` next to
`interactionPlugin()` and `annotationPlugin()`, and turn on a tool:

<Snippet name="annotations/measurements/tool" />

| Tool        | Measures                                      | How                                     |
| :---------- | :-------------------------------------------- | :-------------------------------------- |
| `distance`  | The length of a line                          | Drag from one point to the other        |
| `perimeter` | The length of a path of several lines         | Click each point, double-click to end   |
| `area`      | The area of a shape                           | Click each corner, double-click to end  |

Each measurement is an annotation with its value as a label, so it shows the same in Acrobat and other
PDF apps, and you can select, move and restyle it like any annotation. The example measures the
cover's width at 1:100; pick a tool and measure something else:

<Example name="measurement/basic" mode="open" />

## Set the scale

A page starts at the default scale, 1:1 in meters, unless the PDF already has one. Most plans say
their scale, such as 1:100. Pick it from a list:

```ts
measurement.listPresets(); // 1:1, 1:10, 1:20, 1:50, 1:100, 1:200, 1/4 in = 1 ft, 1/8 in = 1 ft
await measurement.setPreset([page.ref], 'metric-100');
await measurement.setPreset('all', 'metric-100'); // every page
```

Measurements already on the page are recalculated for the new scale. Change the scale, the units
or the precision, and the two measurements follow:

<Example name="measurement/scale" />

## Calibrate from a known length

When the scale isn't printed, measure something you know, such as a door that's 90 centimeters wide:

```ts
measurement.startCalibration(); // the next line the user draws is the known length
```

The plugin asks your UI for the real length, through<Fw only={['react', 'vue', 'svelte']}> `useMeasurementState().calibrationRequest`</Fw><Fw only="angular"> the `measurement.calibrationRequest()` signal</Fw>:

<Snippet name="annotations/measurements/calibration" />

`calibrate()` sets the page's scale from the two points and the length you give. Pass
`{ applyTo: 'all' }` to use the same scale on every page.

<Example name="measurement/calibrate" />

## Units and precision

```ts
await measurement.setUnit([page.ref], 'ft');
await measurement.setAreaUnit([page.ref], 'ft2'); // or 'm2', 'ha', 'acre'
await measurement.setPrecision([page.ref], 100); // two decimals
```

`listUnits()` and `listAreaUnits()` give you the choices for a menu. Precision is 1, 10, 100 and so on:
the number of steps per unit.

## Read a measurement

<Snippet name="annotations/measurements/readout" />

`label` is the text shown on the page, in the page's units. Without a scale to compute from, a
readout says why: `{ unavailable: 'no-scale' }`.

<Example name="measurement/readout" />

To measure without drawing anything, for a tooltip that follows the pointer:

```ts
measurement.measureDistance(page.ref, from, to); // { value, label }
measurement.measureArea(page.ref, corners);
```

`usePageScale(page.ref)` gives you a page's scale, for a scale bar or a "1:100" badge.

## Measure from code

```ts
const { annotation } = await measurement.createMeasurement({
  kind: 'area',
  page: page.ref,
  points: [
    { x: 100, y: 100 },
    { x: 300, y: 100 },
    { x: 300, y: 250 },
  ],
});
```

It makes the same annotation the tool would, with the page's scale and the tool's style.

<Example name="measurement/from-code" />

## Methods

Everything `useMeasurement()` gives you.

| Method                                                   | What it does                                                                                                                                                        |
| :------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `listPresets()`                                          | The scales to pick from, each with an `id` and a `label` such as `1:100`. [Set the scale](#set-the-scale)                                                          |
| `setPreset(pages, presetId)`                             | Gives pages a scale from the list. [Set the scale](#set-the-scale)                                                                                                  |
| `setScale(pages, scale)`                                 | Gives pages a scale of your own, in the form the [engine](/docs/engine/annotations/measurements) reads and writes                                                  |
| `clearScale(pages)`                                      | Puts pages back to the default scale                                                                                                                                |
| `startCalibration()`                                     | Makes the next line the user draws the known length, then asks your UI for its real length. [Calibrate from a known length](#calibrate-from-a-known-length)        |
| `calibrate(request, { applyTo? })`                       | Sets the scale from two points and the real length between them, on their page or on the pages in `applyTo`                                                         |
| `dismissCalibration()`                                   | Cancels calibrating and drops the request                                                                                                                           |
| `setUnit(pages, unit)`, `setAreaUnit(pages, unit)`       | Changes the unit lengths, or areas, are shown in. [Units and precision](#units-and-precision)                                                                       |
| `setPrecision(pages, precision)`                         | Changes how finely a label rounds: `100` shows two decimals                                                                                                        |
| `listUnits()`, `listAreaUnits()`                         | The length and area units to pick from, for a menu                                                                                                                  |
| `getPageScale(page)`                                     | A page's scale, whether it came from the file or is the default, and whether it has loaded. In a component, use `usePageScale(page)`.                              |
| `ensureLoaded(page)`                                     | Loads a page's scale. Resolves at once when it's already known.                                                                                                     |
| `getReadout(ref)`                                        | A measurement's `{ kind, value, label }`, or `{ unavailable }` with why there's none. In a component, use `useMeasurementReadout(ref)`. [Read a measurement](#read-a-measurement) |
| `measureDistance(page, from, to)`, `measureArea(page, corners)` | Measures in the page's scale without drawing anything: `{ value, label }`                                                                                   |
| `createMeasurement({ kind, page, points })`              | Makes the same measurement the tool would. Resolves `{ annotation }`. [Measure from code](#measure-from-code)                                                     |
| `canCalibrate()`, `canMeasure(page)`                     | Whether setting a scale, and measuring on a page, are allowed. [Permissions](#permissions)                                                                         |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()` | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use `useMeasurementSettings()`. [Settings](#settings) |

`pages` is a page, a list of pages, or `'all'`. A scale change recalculates the measurements already
on those pages and resolves one report per page; pass `{ recalculate: false }` to keep their values.
Every method that returns a promise also takes a `signal` in its options, to cancel it.

## State

<StateIntro hook="useMeasurementState()" />

| Field                | What it is                                                                         | Read once                 |
| :------------------- | :--------------------------------------------------------------------------------- | :------------------------ |
| `busy`               | Whether a scale change is still running                                            | `isBusy()`                |
| `calibrationRequest` | The known length the user drew, `{ page, from, to }`, waiting for its real length, or `null` | `getCalibrationRequest()` |
| `lastReports`        | The reports of the last scale change, one per page                                 | `listLastReports()`       |

A page's scale and a measurement's readout come from `usePageScale(page)` and <Fw only={['react', 'vue', 'svelte']}>`useMeasurementReadout(ref)`</Fw><Fw only="angular">`measurement.readoutOf(ref)`</Fw>.<Fw only="react">
Keeping them apart means a component re-renders only for its own page or measurement.</Fw>

## Settings

Set them when you register the plugin, and change them while the app runs with
`measurement.updateSettings({ … })`; `measurement.resetSettings()` goes back to what you registered, and
`useMeasurementSettings()` gives you the current settings.

| Setting        | What it does                                                  | Default                     |
| :------------- | :------------------------------------------------------------ | :-------------------------- |
| `defaultScale` | The scale of a page that has none: `'metric'`, `'imperial'`, or a scale of your own | `'metric'` (1:1 in meters) |
| `presets`      | The scales `listPresets()` offers                             | the eight above             |

A measurement's look, such as its line, arrows and label position, is its tool's defaults; see
[Tools & defaults](tools).

## Events

<Snippet name="annotations/measurements/events" />

| Event                    | Fires when                                         | Carries           |
| :----------------------- | :------------------------------------------------- | :---------------- |
| `onScaleChanged`         | A page's scale changed                             | `page`, `report`  |
| `onCalibrationRequested` | The user drew a known length, and your UI should ask for it | `request` |
| `onCalibrationCompleted` | A page was calibrated                              | `page`            |
| `onCalibrationDismissed` | Calibrating was cancelled                          | —                 |
| `onSettingsChanged` | The settings changed | `settings`, `changed` |

`report` lists the measurements recalculated for the new scale, and any that couldn't be.

## Permissions

| Permission            | What it allows                             | Check                              |
| :-------------------- | :----------------------------------------- | :--------------------------------- |
| `doc.annotate.modify` | Setting a page's scale and calibrating     | `measurement.canCalibrate()`       |
| `annotations:create`  | Measuring                                  | `measurement.canMeasure(page.ref)` |

`canMeasure` is also false while a page's scale is still loading.

## Good to know

### The scale is in the PDF

A page's scale is saved in the file, in the form Acrobat and Bluebeam use, so their measuring tools
read the same scale, and yours read theirs.

### Scales from map software

A few PDFs, such as maps, carry a geographic scale. It's kept as it is, and measurements on such a
page say `{ unavailable: 'foreign-measure' }`. See [Measurements](/docs/engine/annotations/measurements)
for how scales and labels are stored.

---

### Content Headless Annotations Redaction (`docs/content/headless/annotations/redaction.mdx`)

---
title: Redaction
description: Mark text and areas for removal, review the marks, then remove the content for good.
---

# Redaction

Redaction removes content for good: the text, images and drawings under a mark are deleted from the
file, not just covered up. It happens in two steps, as in Acrobat: first people **mark** what should
go, and review it; then they **apply** the marks, and the content is gone.

Register `redactionPlugin()` next to `interactionPlugin()` and `annotationPlugin()`, and use the
`redact` tool: select text to mark it, or drag to mark an area:

<Snippet name="annotations/redaction/bar" />

<Example name="redaction/basic" mode="open" />

A mark is an annotation, so until it's applied, people can move it, resize it, delete it, and see it
in other PDF apps. Applying it is what removes the content.

## Mark from code

```ts
await redaction.markSelection(); // the selected text
await redaction.markArea(page.ref, { x: 72, y: 100, width: 200, height: 40 });
await redaction.markPage(page.ref); // the whole page
await redaction.markMatches({ text: 'Jane Doe' }); // every match in the document
await redaction.markMatches({ text: '\\d{3}-\\d{2}-\\d{4}', regex: true }); // like a social security number
```

`markMatches` takes the same query as [search](../text/search), and resolves with `{ marks }`, one per
match.

<Example name="redaction/from-code" />

## Review the marks

`usePendingRedactions()` lists the marks that haven't been applied yet, for a review panel:

<Snippet name="annotations/redaction/pending" />

`redaction.clearPending()` removes every mark.<Fw only={['react', 'vue', 'svelte']}> `usePendingRedactions({ page: page.ref })`
lists one page's.</Fw><Fw only="angular"> `redaction.pendingOn(page.ref)` lists one page's.</Fw>

<Example name="redaction/review" />

## Apply

```ts
const result = await redaction.applyAll(); // every mark
await redaction.apply([mark.ref]); // some marks
await redaction.applyPages([page.ref]); // the marks on some pages
```

This can't be undone. Ask first, and say what else goes with it: annotations a mark overlaps are
removed too, because they could show what was there. `estimateCollateral()` tells you how many:

```ts
const { count } = redaction.estimateCollateral();
if (!(await confirm(`Redact? ${count} annotations under the marks will be removed too.`))) return;
await redaction.applyAll();
```

After applying, download the document with `{ mode: 'rewrite' }`, so the removed content isn't left in
an earlier revision of the file. See [Saving & downloading](../documents/saving).

<Example name="redaction/apply" />

## What shows in place of the content

By default a redacted area is black. Give it a label, such as "Classified", on a mark:

```ts
await redaction.updateLabel(mark.ref, { overlayText: 'Classified', repeat: true });
```

`repeat` fills the area with the label over and over; otherwise it shows once. The color and font of
every mark are settings.

<Example name="redaction/label" />

## Methods

Everything `useRedaction()` gives you.

| Method                                       | What it does                                                                                                                                              |
| :------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `markSelection()`                            | Marks the selected text, one mark per page, and clears the selection. Resolves `{ marks }`. [Mark from code](#mark-from-code)                            |
| `markArea(page, rect)`, `markPage(page)`     | Marks an area of a page, or the whole page. Resolves `{ mark }`.                                                                                          |
| `markMatches(query, { pages? })`             | Marks every match of a [search](../text/search), or the ones on some pages. It replaces the current search. Resolves `{ marks }`.                        |
| `unmark(refs)`                               | Removes marks without applying them. [Review the marks](#review-the-marks)                                                                                |
| `clearPending()`                             | Removes every mark                                                                                                                                        |
| `updateLabel(ref, { overlayText, repeat? })` | Sets the label a mark shows once it's applied; `overlayText: null` clears it. Resolves `{ mark }`. [What shows in place of the content](#what-shows-in-place-of-the-content) |
| `listPending({ page? })`                     | The marks not applied yet, in page order, or one page's. In a component, use `usePendingRedactions()`.                                          |
| `getPending(ref)`                            | One mark, or `null` when it isn't a mark waiting to be applied                                                                                            |
| `estimateCollateral(refs?)`                  | The other annotations applying would remove too, as `{ count, refs }`, for every mark or for the ones you pass. [Apply](#apply)                          |
| `applyAll()`                                 | Applies every mark in the document, on pages that haven't loaded too. Can't be undone. [Apply](#apply)                                                   |
| `apply(refs)`, `applyPages(pages)`           | Applies some marks, or the marks on some pages. Can't be undone.                                                                                          |
| `canMark()`, `canUnmark(ref)`, `canUpdateLabel(ref)`, `canApply()` | Whether marking, removing a mark, changing its label and applying are allowed. [Permissions](#permissions)                          |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()` | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use `useRedactionSettings()`. [Settings](#settings) |

Applying resolves the result: each page's `status` (`'applied'` or `'unchanged'`) and how many
other annotations were removed. Applying is all or nothing: when a page fails, no page changes and
the promise rejects. Every method that returns a promise also
takes a `signal` in its options, to cancel it.

## State

<StateIntro hook="useRedactionState()" />

| Field          | What it is                                                        | Read once                                                |
| :------------- | :---------------------------------------------------------------- | :------------------------------------------------------- |
| `pendingCount` | How many marks are waiting to be applied                          | `getPendingCount()`, or `getPendingCount(page)` for one page |
| `applying`     | Whether marks are being applied right now                         | `isApplying()`                                           |
| `lastResult`   | The result of the last apply, from this session or another, or `null` | `getLastResult()`                                    |

The marks themselves come from `usePendingRedactions()`.<Fw only="react"> Keeping them apart means a component that
only shows the count doesn't re-render for every mark.</Fw>

## Settings

Set them when you register the plugin, and change them while the app runs with
`redaction.updateSettings({ … })`; `redaction.resetSettings()` goes back to what you registered, and
`useRedactionSettings()` gives you the current settings.

| Setting                | What it does                                     | Default     |
| :--------------------- | :----------------------------------------------- | :---------- |
| `overlay.fill`         | The color of a redacted area                     | `'#000000'` |
| `overlay.text.color`   | The color of a label                             | `'#ffffff'` |
| `overlay.text.fontFamily` | The label's font                              | Helvetica   |
| `overlay.text.fontSize`   | The label's size, or `0` to fit the area (a repeated label then draws at 12pt) | `0`         |

## Events

<Snippet name="annotations/redaction/events" />

| Event              | Fires when                                      | Carries             |
| :----------------- | :---------------------------------------------- | :------------------ |
| `onApplied`        | Marks were applied and the content is gone      | `result`, `origin`  |
| `onPendingChanged` | A mark was added, changed or removed            | `pages`             |
| `onSettingsChanged` | The settings changed | `settings`, `changed` |

## Permissions

| Permission            | What it allows                          | Check                   |
| :-------------------- | :-------------------------------------- | :---------------------- |
| `annotations:create`  | Marking                                 | `redaction.canMark()`   |
| `annotations:delete`  | Removing a mark before it's applied     | `redaction.canUnmark(ref)` |
| `annotations:update`  | Changing a mark's label                 | `redaction.canUpdateLabel(ref)` |
| `doc.redact`          | Applying                                | `redaction.canApply()`  |

A mark is an annotation, so removing it or changing its label follows the same rules as any
annotation, per mark. Applying also rewrites the pages and removes annotations, so `canApply()` needs
`doc.pages.modify` and `doc.annotate.modify` as well. A reviewer who may mark but not apply can
prepare redactions for someone else to approve.

## Good to know

### What is removed

The characters under a mark are removed, so they can't be selected, searched or copied; the pixels
of images inside it are erased; and the parts of lines, shapes and fills inside it are cut away. See
[Redaction](/docs/engine/documents/redaction) for the details.

### Where the original is

<Engine local>

The local engine redacts a copy of the file the browser already has, so the original was on the
user's device before it was redacted.

<CloudPdfCallout
  title="Redact before anyone receives the file"
  href="https://www.cloudpdf.com/docs/headless/annotations/redaction"
  cta="Redact with CloudPDF"
>

With CloudPDF, redaction happens on your server. People who may only see the redacted version never
receive the original: not the file, not its text, not its images.

</CloudPdfCallout>

</Engine>

<Engine cloud>

Redaction happens on your server, and people who may only see the redacted version never receive the
original.

</Engine>

---

### Content Headless Annotations Saving (`docs/content/headless/annotations/saving.mdx`)

---
title: Events, saving & sharing
description: Listen for annotation changes, save them into the PDF, and move them in and out of your own database.
---

# Events, saving & sharing

Every annotation change fires an event, whoever made it: the user, your code, or someone else on
a shared document. Listen for them to keep your own UI or server in step:

<Snippet name="annotations/saving/events" />

<Example name="annotation/events" />

> **Try it:** draw, move and delete. Each change is logged once the engine has saved it, as
> `local`: this viewer made it.

<Fw only={['react', 'vue', 'svelte']}>

The <Word of="hook" /> listens while the component is mounted. Outside a component, call the event on the
plugin's API; it returns a function that stops listening:

```ts
const stop = annotation.onCreated(({ annotation }) => {
  /* … */
});
```

</Fw>

<Fw only="angular">

`takeUntilDestroyed()` stops listening when the component or directive is destroyed. Anywhere
else, such as in a service, keep the subscription and end it yourself:

```ts
const subscription = annotation.created$.subscribe(({ annotation }) => {
  /* … */
});

subscription.unsubscribe();
```

</Fw>

## Events

| Event               | Fires when                                                          | Carries                   |
| :------------------ | :------------------------------------------------------------------ | :------------------------ |
| `onCreated`         | An annotation was added.                                            | `annotation`, `origin`    |
| `onUpdated`         | An annotation was changed.                                          | `annotation`, `origin`    |
| `onDeleted`         | Annotations were deleted: one, plus its popup and replies.          | `refs`, `page`, `origin`  |
| `onReordered`       | The drawing order on a page changed.                                | `page`, `order`, `origin` |
| `onResynced`        | Annotations were read again, as after loading a document.           | `pages`                   |
| `onWriteFailed`     | The engine refused a change the user made.                          | `refs`, `error`           |
| `onSettingsChanged` | A setting changed, through `updateSettings()` or `resetSettings()`. | `settings`, `changed`     |

- **Once, and after it's saved.** <Fw only={['react', 'vue', 'svelte']}>`onCreated`, `onUpdated`, `onDeleted` and
  `onReordered`</Fw><Fw only="angular">`created$`, `updated$`, `deleted$` and `reordered$`</Fw> fire when the engine has saved
  the change, never while it's still pending. A change fires once, however
  many screens show it.
- **Loading isn't a list of creates.** When a document opens, its annotations arrive as one
  <Fw only={['react', 'vue', 'svelte']}>`onResynced`, not as an `onCreated`</Fw><Fw only="angular">`resynced$`, not as a `created$`</Fw> per
  annotation. The same happens after a change that touches whole pages, such as flattening or
  redaction. Then read the `pages` it names again with `list()`.
- **`annotation` is the record after the change.** It's what `get()` returns now.
- **`order` is the whole page.** After a reorder, `order` holds the page's annotations as refs,
  bottom to top, the same order `list({ pages: [page] })` returns.

The selection, a shape being drawn, text editing and hover have events of their own
(<Fw only={['react', 'vue', 'svelte']}>`onSelectionChanged`, `onDraftChanged`, `onEditingChanged`, `onHoverChanged`</Fw><Fw only="angular">`selectionChanged$`,
`draftChanged$`, `editingChanged$`, `hoverChanged$`</Fw>); see [Selecting & editing](selecting#events).
A tool's defaults have <Fw only={['react', 'vue', 'svelte']}>`tools.onDefaultsChanged`</Fw><Fw only="angular">`tools.defaultsChanged$`</Fw>; see
[Tools & defaults](tools#events).

## Where a change came from

`origin` says who made the change:

| Field         | What it is                                                                    |
| :------------ | :---------------------------------------------------------------------------- |
| `origin.kind` | `'local'` when this viewer made the change, `'remote'` when someone else did. |
| `origin.sub`  | The user who made it (on a shared document).                                  |
| `origin.ts`   | When it happened, in milliseconds.                                            |
| `origin.tx`   | Shared by every change one call made, such as an import.                      |

<Snippet name="annotations/saving/remote-toast" />

These are the engine's own events, so the fields mean the same as on
[Events](/docs/engine/concepts/events).

## Save the PDF

Annotations are part of the document. Save the document and they're in the file:

```ts
const bytes = await documents.download(); // the whole PDF, with every annotation
```

<Example name="annotation/download" />

A save waits for pending changes, so what you save is what the user sees. For the difference
between a quick save that appends the changes and a full rewrite, see
[Saving](/docs/engine/documents/saving).

## Export and import

To take annotations out of a document, as data rather than inside a PDF, export them. One call
gives you a **bundle**: every annotation with the bytes it carries (a stamp's drawing, an
attached file). One call puts a bundle into a document. `AnnotationTransfer` comes from
`@embedpdf/react/annotation`:

```ts
const bundle = await annotation.export(); // everything
const text = AnnotationTransfer.stringify(bundle); // one JSON text, ready to store

// later, in the same document or another one
const { annotations, dropped } = await annotation.import(await AnnotationTransfer.parse(text), {
  pages: 'by-position',
});
```

Use it to:

- keep annotations in your own database, apart from the PDF;
- carry a review over to a new version of a document;
- let a user paste annotations from one document into another.

<Example name="annotation/export-import" />

> **Try it:** **Delete all**, then **Import**: they come back from the text. Import again: the
> annotations with a name are left out, since they're there already.

Export waits for pending changes, and imported annotations show at once, like any other change.

What to export:

```ts
await annotation.export({ refs: annotation.selection.list().map((a) => a.ref) }); // the selection
await annotation.export({ pages: [page.ref] }); // one page
```

A selection brings what it needs: a reply brings the comment it answers, and a comment brings its
replies.

How to import:

| Option        | What it does                                                                                     | Default     |
| :------------ | :----------------------------------------------------------------------------------------------- | :---------- |
| `pages`       | Where each page's annotations go: `'same'` (the same page), `'by-position'`, or a list of pairs. | `'same'`    |
| `attribution` | `'restore'` keeps who wrote each annotation, `'stamp'` makes the importing user the author.      | `'restore'` |

Use `'stamp'` when a user pastes annotations: they become theirs. `'restore'` writes annotations
in other people's names, so it needs a special permission; keep it for trusted jobs, such as
loading from your own database.

The result tells you what happened:

- **`annotations`**: the new annotations, in bundle order.
- **`refMap`**: each annotation's ref in the bundle, and its new ref here.
- **`dropped`**: what was left out, and why. For example, an annotation whose name is already
  taken on its page. Importing the same bundle twice doesn't duplicate annotations with a name.

The engine's [Import & export](/docs/engine/annotations/import-export) page has every option,
reason and limit.

## Keep annotations in your own database

There are two ways to store annotations outside the PDF.

**Store a bundle per document.** Export when the user saves, store the text, and import it when
the document opens again. It's one row per document and one call each way:

```ts
// on save
await api.put(`/reviews/${docId}`, AnnotationTransfer.stringify(await annotation.export()));

// on open, once the annotations are loaded (status 'ready')
const text = await api.get(`/reviews/${docId}`);
if (text) await annotation.import(await AnnotationTransfer.parse(text));
```

**Store each annotation as it changes.** Listen for the events and write each change to your
server. Key your rows by the annotation's ref:

<Snippet name="annotations/saving/sync" />

<Fw only={['react', 'vue', 'svelte']}>Handle `onUpdated` the same way, and `onDeleted` by deleting each of its `refs`. On
`onResynced`, compare the pages it names with `list()`.</Fw><Fw only="angular">Handle `updated$` the same way, and
`deleted$` by deleting each of its `refs`. On `resynced$`, compare the pages it names with
`list()`.</Fw> Only send `local` changes: a `remote` one was already saved by whoever made it.

### Which id to store

Store the annotation's `ref`. It's plain data, and it stays valid after the document is saved
and opened again. To use it as a key in a map or a URL, turn it into a string with
`annotationKey(ref)`. See [Refs & identity](/docs/engine/concepts/identity).

## Good to know

### Unsaved changes

To warn before someone leaves with annotations that aren't downloaded yet, use
<Fw only={['react', 'vue', 'svelte']}>`hasUnsavedChanges` from `useDocument()`</Fw><Fw only="angular">the `hasUnsavedChanges()` signal on
`inject(EpdfDocument)`</Fw>; see [Warn before changes are lost](../documents/saving#warn-before-changes-are-lost).

### Events on a shared document

<Engine local>

The local engine's events come from this tab: the changes made here, by the user or your code.

<CloudPdfCallout
  title="Review one document together, live"
  href="https://www.cloudpdf.com/docs/headless/annotations/saving"
  cta="Collaborate with CloudPDF"
>

With CloudPDF, annotations are stored on your server as people make them, and everyone with the
document open gets each other's changes as events, with `origin.kind: 'remote'`. The server checks
that people only change what they may, such as their own annotations. Your code stays the same: the
same events, the same `list()`.

</CloudPdfCallout>

</Engine>

<Engine cloud>

Every viewer with the document open gets every change as an event, with `origin.kind` set to
`'remote'` for the ones it didn't make. The annotations appear in `list()` by themselves; the events
are there so you can react, such as with a notification.

</Engine>

---

--- METRICS ---
- Files Extracted: 101
- Estimated Token Budget: ~234336 tokens
- Canonical Reference: https://codewiki.google/github.com/embedpdf/embed-pdf-viewer
