# PDFViewer

The viewer: renders a `PDFDocument` inside a DOM container and wires its pages, form fields, toolbar, sidebar and tools. It takes a `PDFViewerConfig`; `viewer.config` is that configuration resolved over the defaults, with what the license does not cover switched off.

The viewer is on the page when the constructor returns; it is ready — pages laid out, fields and tools wired — a moment later, which [whenReady()](https://kernstra.com/docs/web-sdk/api/pdfviewer/whenready) resolves at and [isReady](https://kernstra.com/docs/web-sdk/api/pdfviewer/isready) reports. Every member that acts throws `INVALID_STATE` after [destroy()](https://kernstra.com/docs/web-sdk/api/pdfviewer/destroy); the getters keep answering their last values. Which license feature each member needs is stated on the member; a member without one needs nothing beyond an open document.

- Kind: class
- Access: `new PDFViewer(container, doc, config?)`

## Usage

```ts
import { Kernstra, PDFDocument, PDFViewer } from '@kernstra/pdf-web-sdk';

await Kernstra.init({ license: import.meta.env.KERNSTRA_LICENSE });

const container = document.getElementById('viewer')!;
const hideLoader = PDFViewer.showLoadingIndicator(container);
const doc = await PDFDocument.fromUrl('/report.pdf');
const viewer = new PDFViewer(container, doc, { mode: 'form-fill' });
await viewer.whenReady();
hideLoader();

viewer.on('pageChanged', ({ pageIndex }) => setCurrentPage(pageIndex));

// Later
viewer.destroy();
await doc.close();
```

## Constructor

```ts
new PDFViewer(container: HTMLElement, document: PDFDocument, config?: PDFViewerConfig)
```

Throws `NOT_INITIALIZED` before `Kernstra.init()` has resolved, `DOCUMENT_CLOSED` for a document already closed, `INVALID_ARGUMENT` when `container` is not an element (a ref not yet set), `INVALID_STATE` when the container already holds a viewer (destroy that one first — a component that re-renders builds one viewer per element, never a second into the same one), and `ENGINE_FAILED` when the SDK has stopped on an unrecoverable fault (fatal). The viewer is on the page when the constructor returns; it is ready a moment later, which `whenReady()` resolves at. A loader `PDFViewer.showLoadingIndicator` put in the container stays over the viewer until you remove it, so "show the loader, open, construct, `await viewer.whenReady()`, hide" is the whole sequence. If `new PDFViewer()` throws, the viewer has already undone whatever it had set up; there is nothing to destroy.

| Parameter | Type | Description |
| --- | --- | --- |
| `container` | `HTMLElement` | The element the viewer is rendered into. |
| `document` | `PDFDocument` | An open `PDFDocument`. |
| `config?` | `PDFViewerConfig` | The viewer configuration; your options over the defaults. |

## Properties

### `currentPageIndex`

```ts
get currentPageIndex(): number
```

The page on show, as a zero-based index — what [pageChanged](https://kernstra.com/docs/web-sdk/api/events#ev-pagechanged) last named.

- Kind: getter
- Since: 1.0.0

Returns `number`

```ts
label.textContent = `Page ${viewer.currentPageIndex + 1} of ${doc.pageCount}`;
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/currentpageindex

### `scale`

```ts
get scale(): number
```

Current zoom scale (1.0 = 100%).

- Kind: getter
- Since: 1.0.0

Returns `number`

```ts
zoomLabel.textContent = `${Math.round(viewer.scale * 100)}%`;
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/scale

### `config`

```ts
get config(): Readonly<ResolvedPDFViewerConfig>
```

Resolved configuration: your options over the defaults, with what the license does not cover switched off (its tools, and `toolbar.print` / `download`). Its `tools` names every tool: `false` for one you, the mode or the license switched off, otherwise your options for it or `true`. Follows the rights of the viewer's document: grows at once when they grow, and narrows only when pages are imported or another document is opened.

- Kind: getter
- Since: 1.0.0

Returns `Readonly<ResolvedPDFViewerConfig>`

```ts
const canSign = viewer.config.tools.sign !== false;
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/config

### `tools`

```ts
get tools(): ToolsApi
```

Handles for running tools — see [viewer.tools.editText](https://kernstra.com/docs/web-sdk/api/viewer-tools-edittext). A handle throws `SDKError` `INVALID_STATE`, with instructions, when its tool is not running, rather than failing silently.

- Kind: getter
- Since: 1.0.0

Returns `ToolsApi`

```ts
await viewer.activateTool('editText');
const blocks = await viewer.tools.editText.listBlocks(0);
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/tools

### `hasUnsavedChanges`

```ts
get hasUnsavedChanges(): boolean
```

`true` from the first change on — a tool's, a page's, a field filled in or signed, the document API's, an undo — until the document is exported whole by an exporter from [createExporter()](https://kernstra.com/docs/web-sdk/api/pdfviewer/createexporter) (the toolbar's Download; `download()`, `getBlob()`, `getArrayBuffer()`, `getBase64()`, `uploadTo()` without `pages`) or [markSaved()](https://kernstra.com/docs/web-sdk/api/pdfviewer/marksaved) is called. A change made while an export ran stays unsaved; an undo back to the saved state still counts. [documentChanged](https://kernstra.com/docs/web-sdk/api/events#ev-documentchanged) reports every change of it; nothing reports it going back to `false`. The export is the "save": a host that uploads the bytes keeps its own flag for an upload that failed, or a "leave without saving?" guard lets the user go after one — see [Saving as the user works](https://kernstra.com/docs/web-sdk/guides/replace).

- Kind: getter
- Since: 1.0.0

Returns `boolean`

```ts
window.addEventListener('beforeunload', (e) => {
  if (viewer.hasUnsavedChanges) e.preventDefault();
});
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/hasunsavedchanges

### `isReady`

```ts
get isReady(): boolean
```

Whether the viewer is ready: its pages laid out and its tools wired. [whenReady()](https://kernstra.com/docs/web-sdk/api/pdfviewer/whenready) resolves at that moment.

- Kind: getter
- Since: 1.0.0

Returns `boolean`

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/isready

### `activeTool`

```ts
get activeTool(): ToolName | null
```

The active tool's name — `'selectText'` at rest by default, from the moment the viewer goes back to it — or `null` when no tool is active. A property, read like `viewer.scale`.

- Kind: getter
- Since: 1.0.0

Returns `ToolName | null`

```ts
const editing = viewer.activeTool === 'editText';
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/activetool

### `mode`

```ts
get mode(): SDKMode
```

The viewer's operating mode, as configured — see [SDKMode and the resting tool](https://kernstra.com/docs/web-sdk/config/mode).

- Kind: getter
- Since: 1.0.0

Returns `SDKMode`

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/mode

### `activePanel`

```ts
get activePanel(): SidebarTab | null
```

The open sidebar panel, or `null` when the sidebar is collapsed. The [panelChanged](https://kernstra.com/docs/web-sdk/api/events#ev-panelchanged) event reports every change of it.

- Kind: getter
- Since: 1.0.0

Returns `SidebarTab | null`

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/activepanel

## Navigation

### `goToPage`

```ts
goToPage(pageIndex: number): void
```

Scroll to page (zero-based index).

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `pageIndex` | `number` | Zero-based page index. |

Returns `void`

Throws:

- `INVALID_ARGUMENT` for an index that is not a whole number or names no page.
- `INVALID_STATE` after `destroy()`.
- `DOCUMENT_CLOSED` once the document has been closed.

```ts
viewer.goToPage(doc.pageCount - 1); // the last page
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/gotopage

### `nextPage`

```ts
nextPage(): void
```

Navigate to next page. Stops at the end.

- Kind: method
- Since: 1.0.0

Returns `void`

Throws:

- `INVALID_STATE` after `destroy()`.

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/nextpage

### `prevPage`

```ts
prevPage(): void
```

Navigate to previous page. Stops at the first page.

- Kind: method
- Since: 1.0.0

Returns `void`

Throws:

- `INVALID_STATE` after `destroy()`.

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/prevpage

## Zoom

### `setScale`

```ts
setScale(scale: number, anchor?: { clientX: number; clientY: number }): void
```

Set zoom level (1.0 = 100%). The same scale again changes nothing. The pages are redrawn at the new scale a moment after the call returns.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `scale` | `number` | The zoom level (1.0 = 100%). A positive number. |
| `anchor?` | `{ clientX: number; clientY: number }` | Keeps the point under it in place; without it, the middle of the view. |

Returns `void`

Throws:

- `INVALID_ARGUMENT` for a scale that is not a positive number.
- `INVALID_STATE` after `destroy()`.

```ts
viewer.setScale(1.5);
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/setscale

### `zoomIn`

```ts
zoomIn(): void
```

One step, as the toolbar's buttons and ⌘/Ctrl + do: to the next preset of [navigation.zoomPresets](https://kernstra.com/docs/web-sdk/config/navigation/zoompresets) above the current scale, then by steps up to [navigation.maxZoom](https://kernstra.com/docs/web-sdk/config/navigation/maxzoom). Leaves a fit mode.

- Kind: method
- Since: 1.0.0

Returns `void`

Throws:

- `INVALID_STATE` after `destroy()`.

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/zoomin

### `zoomOut`

```ts
zoomOut(): void
```

One step, as the toolbar's buttons and ⌘/Ctrl − do: to the next preset of [navigation.zoomPresets](https://kernstra.com/docs/web-sdk/config/navigation/zoompresets) below the current scale, then by steps down to [navigation.minZoom](https://kernstra.com/docs/web-sdk/config/navigation/minzoom). Leaves a fit mode.

- Kind: method
- Since: 1.0.0

Returns `void`

Throws:

- `INVALID_STATE` after `destroy()`.

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/zoomout

### `fitToWidth`

```ts
fitToWidth(): void
```

Zoom to fit page width in container.

- Kind: method
- Since: 1.0.0

Returns `void`

Throws:

- `INVALID_STATE` after `destroy()`.

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/fittowidth

### `fitToPage`

```ts
fitToPage(): void
```

Zoom to fit entire page in viewport.

- Kind: method
- Since: 1.0.0

Returns `void`

Throws:

- `INVALID_STATE` after `destroy()`.

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/fittopage

## Tools

### `activateTool`

```ts
activateTool(tool: ToolName): Promise<void>
```

Activate a tool (again: put it away, back to the resting tool). Edit Text left with edits pending writes them first; a refused write keeps it, and the promise resolves with it still active. Needs the tool's license feature; refused for a tool the license does not cover.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `tool` | `ToolName` | One of the 26 names of `TOOL_NAMES`, in the toolbar's order. Two of them are panels rather than tools: `'search'` and `'managePages'` open their panel and resolve, and `activeTool` stays what it was — `openPanel('search')` / `openPanel('pages')` are the same action by their own name. The exported `PANEL_TOOL_NAMES` names those two, so a host that marks its buttons pressed from `activeTool` or `toolChanged` need not hard-code them. |

Returns `Promise<void>`

Throws:

- `INVALID_ARGUMENT` for a name that is no tool or a tool that is off — see [isToolEnabled](https://kernstra.com/docs/web-sdk/api/pdfviewer/istoolenabled).
- `FEATURE_NOT_LICENSED` for one the license does not cover.
- `PERMISSION_DENIED` for one the document's permissions forbid — see [toolBlockedReason](https://kernstra.com/docs/web-sdk/api/pdfviewer/toolblockedreason).
- `UNKNOWN_ERROR` (the cause as its `cause`; or the tool's own `SDKError`) for a tool that could not start — nothing of it stays, and the viewer goes back to the resting tool.
- `INVALID_STATE` after `destroy()`.

```ts
await viewer.activateTool('highlight');
await viewer.activateTool('highlight'); // again: back to the resting tool
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/activatetool

### `deactivateTool`

```ts
deactivateTool(): Promise<void>
```

Put the active tool away and go back to the resting tool ([defaultTool](https://kernstra.com/docs/web-sdk/config/mode/defaulttool), Select by default); with the resting tool active, put down what is selected — an annotation picked up, the text Select selected. Edit Text with edits pending writes them first, as Done does, and the promise resolves once the tool is away; a refused write keeps the tool (`activeTool` tells). With nothing pending the tool is away before the call returns.

- Kind: method
- Since: 1.0.0

Returns `Promise<void>`

Throws:

- `INVALID_STATE` after `destroy()`.

```ts
await viewer.deactivateTool();
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/deactivatetool

### `isToolEnabled`

```ts
isToolEnabled(tool: ToolName): boolean
```

Whether a tool may run: covered by the license, not `false` in `tools`, and offered by the mode (Select, Pan and Search in every mode, Sign in `FORM_FILL` and `FULL`, the rest in `FULL` only). `toolbar.groups` does not affect it. Mirror it in your own chrome instead of duplicating the rule.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `tool` | `ToolName` | The tool to ask about. |

Returns `boolean`

```ts
signButton.hidden = !viewer.isToolEnabled('sign');
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/istoolenabled

### `toolBlockedReason`

```ts
toolBlockedReason(tool: ToolName): ToolBlockedReason | null
```

Why the open document forbids a tool — `{ reason, message }`: `reason` is `'permission'` (the document's permissions), `'certified'` (its certification) or `'downloading'` (it edits, and the document's bytes are still arriving), `message` the words in the viewer's language ("This document does not allow comments") — or `null` when it allows it. A tool it forbids is still offered, grayed out with the message. A host drawing its own chrome shows the same, and branches on `reason`.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `tool` | `ToolName` | The tool to ask about. |

Returns `ToolBlockedReason | null`: `{ reason, message }`, or `null` when the document allows it (and on a destroyed viewer).

```ts
const blocked = viewer.toolBlockedReason('note');
noteButton.disabled = blocked !== null;
noteButton.title = blocked?.message ?? '';
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/toolblockedreason

## History

### `snapshot`

```ts
snapshot(): Promise<void>
```

Save current document state for undo (called automatically after committed edits). Skipped, with an `error` event, while a progressively-loaded document is still downloading — so undo has a gap until [whenFullyLoaded()](https://kernstra.com/docs/web-sdk/api/pdfdocument/whenfullyloaded) resolves. A change whose undo step could not be recorded (an `error` with `EXPORT_FAILED`) still counts: `hasUnsavedChanges` turns true and `documentChanged` reports it. A change made through the document API while Edit Text has unwritten edits writes those edits first, then reloads; if they cannot be written, the viewer reports `INVALID_STATE` on `error` and the edits are lost.

- Kind: method
- Since: 1.0.0

Returns `Promise<void>`: Resolves once recorded.

Throws:

- `INVALID_STATE` after `destroy()`.

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/snapshot

### `undo`

```ts
undo(): Promise<void>
```

Restore previous document state. Edit Text edits pending are written first, as one step, and that step is what this undo backs out; nothing is undone when the write is refused.

- Kind: method
- Since: 1.0.0

Returns `Promise<void>`: Resolves once restored.

Throws:

- `INVALID_STATE` after `destroy()`.

```ts
if (viewer.canUndo()) await viewer.undo();
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/undo

### `redo`

```ts
redo(): Promise<void>
```

Re-apply previously undone state. Edit Text edits pending are written first, as for `undo()`.

- Kind: method
- Since: 1.0.0

Returns `Promise<void>`: Resolves once re-applied.

Throws:

- `INVALID_STATE` after `destroy()`.

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/redo

### `canUndo`

```ts
canUndo(): boolean
```

Returns `true` if undo is available. For buttons that follow the stack, listen to [documentChanged](https://kernstra.com/docs/web-sdk/api/events#ev-documentchanged), which carries `canUndo` and `canRedo`.

- Kind: method
- Since: 1.0.0

Returns `boolean`

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/canundo

### `canRedo`

```ts
canRedo(): boolean
```

Returns `true` if redo is available.

- Kind: method
- Since: 1.0.0

Returns `boolean`

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/canredo

### `markSaved`

```ts
markSaved(): void
```

Tells the viewer the document as it stands is saved: `hasUnsavedChanges` is `false` until the next change. Called for you when the document is exported whole; call it yourself after saving through [doc.save()](https://kernstra.com/docs/web-sdk/api/pdfdocument/save) or your own path.

- Kind: method
- Since: 1.0.0

Returns `void`

Throws:

- `INVALID_STATE` after `destroy()`.

```ts
const bytes = await doc.save();
await fetch('/documents/42', { method: 'PUT', body: bytes.slice() });
viewer.markSaved();
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/marksaved

## Search

### `search`

```ts
search(query: string): Promise<void>
```

Open the search panel and run a query across all pages. **Results are shown in the panel, not returned**: it is a UI action. For matches as data, use [doc.text.search()](https://kernstra.com/docs/web-sdk/api/doc-text/search). Needs the `extract` feature: its highlights and count say where the words are (the search panel the user types in needs no feature).

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `query` | `string` | The words to look for. |

Returns `Promise<void>`: Resolves once the search has run.

Throws:

- `FEATURE_NOT_LICENSED` without `extract`.
- `INVALID_STATE` after `destroy()`.

```ts
await viewer.search('refund');
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/search

### `clearSearch`

```ts
clearSearch(): void
```

Close search panel and clear highlights.

- Kind: method
- Since: 1.0.0

Returns `void`

Throws:

- `INVALID_STATE` after `destroy()`.

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/clearsearch

## Forms

### `focusField`

```ts
focusField(name: string): boolean
```

Scroll a field into view (any page) and focus its editor. Values are read and set through [doc.forms](https://kernstra.com/docs/web-sdk/api/doc-forms), with or without a viewer. While this viewer is open they go through its fields: shown at once, on its undo stack, in its export, and [formFieldChanged](https://kernstra.com/docs/web-sdk/api/events#ev-formfieldchanged) fires once per applied field.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `name` | `string` | The field's fully qualified name. |

Returns `boolean`: `false` if unknown.

Throws:

- `INVALID_STATE` after `destroy()`.

```ts
viewer.focusField('applicant.email');
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/focusfield

## Comments and panels

### `showComment`

```ts
showComment(id: string): Promise<boolean>
```

Open the Comments panel on the comment with that id (a reply's or a status's id opens its comment): its card chosen, its mark scrolled to and outlined.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | `string` | A comment's id; a reply's or a status's id opens its comment. |

Returns `Promise<boolean>`: `false` if no comment has it or the panel is not offered.

Throws:

- `INVALID_STATE` after `destroy()`.

```ts
await viewer.showComment(comment.id);
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/showcomment

### `commentPermissions`

```ts
commentPermissions(id: string): Promise<CommentPermissions | null>
```

What the user may do to a comment by the panel's rules — `{ reply, setStatus, edit, delete }`; `null` when no comment has that id (none, or a link, form field, pop-up or hidden annotation).

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | `string` | The comment's id. |

Returns `Promise<CommentPermissions | null>`: `{ reply, setStatus, edit, delete }`, each a boolean.

Throws:

- `INVALID_STATE` after `destroy()`.

```ts
const may = await viewer.commentPermissions(id);
deleteButton.hidden = may?.delete !== true;
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/commentpermissions

### `openPanel`

```ts
openPanel(panel: SidebarTab | null): boolean
```

Open a sidebar panel, or close the sidebar with `null`. Which panels are offered is the [sidebar](https://kernstra.com/docs/web-sdk/config/sidebar) option.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `panel` | `SidebarTab \| null` | `'pages'`, `'outline'`, `'attachments'`, `'signatures'` or `'comments'`; `null` closes the sidebar. |

Returns `boolean`: `false` if that panel is not offered.

Throws:

- `INVALID_STATE` after `destroy()`.

```ts
viewer.openPanel('comments');
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/openpanel

## Coordinates

### `pageToClient`

```ts
pageToClient<T extends Rect | Point>(value: T, pageIndex: number): T | null
```

A point or rectangle of a page, in page space, as it lies in the viewer now: CSS pixels from the container's top-left corner at the current zoom, scroll and rotation (a `Rect` comes back with its top-left corner as `x, y`, as the DOM measures). Convert again after [zoomChanged](https://kernstra.com/docs/web-sdk/api/events#ev-zoomchanged), a scroll or a resize. Page space and the geometry helpers are described under [Helpers](https://kernstra.com/docs/web-sdk/api/helpers).

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `value` | `Rect \| Point` | A point or rectangle in page space. |
| `pageIndex` | `number` | Zero-based page index. |

Returns `Rect | Point | null`: `null` before the page is laid out, or on a destroyed viewer.

Throws:

- `INVALID_ARGUMENT` for a page index the document has not, or a value that is neither a `Point` nor a `Rect` of finite numbers.

```ts
const [field] = await doc.forms.fields(0);
const box = field ? viewer.pageToClient(field.rect, 0) : null;
if (box) {
  badge.style.transform = `translate(${box.x}px, ${box.y}px)`;
}
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/pagetoclient

### `clientToPage`

```ts
clientToPage<T extends Rect | Point>(value: T, pageIndex: number): T | null
```

The reverse of [pageToClient](https://kernstra.com/docs/web-sdk/api/pdfviewer/pagetoclient): CSS pixels from the container's top-left corner, as the page holds them.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `value` | `Rect \| Point` | CSS pixels from the container's top-left corner. |
| `pageIndex` | `number` | Zero-based page index. |

Returns `Rect | Point | null`: `null` before the page is laid out, or on a destroyed viewer.

Throws:

- `INVALID_ARGUMENT` for a page index the document has not, or a value that is neither a `Point` nor a `Rect` of finite numbers.

```ts
container.addEventListener('click', (e) => {
  const bounds = container.getBoundingClientRect();
  const at = viewer.clientToPage({ x: e.clientX - bounds.left, y: e.clientY - bounds.top }, 0);
  // at: where on page 0 the user clicked, in page space, or null
});
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/clienttopage

## Export and print

### `createExporter`

```ts
createExporter(config?: ExportConfig): PDFExporter
```

A [PDFExporter](https://kernstra.com/docs/web-sdk/api/pdfexporter) wired to this viewer's form values and signatures. Its defaults are the viewer's `export` option; `config` overrides them. Obtain the exporter here rather than with `new PDFExporter(document)`: it carries what the viewer holds and reports `exportStarted` / `exportCompleted` on the viewer, and an export of the whole document clears [hasUnsavedChanges](https://kernstra.com/docs/web-sdk/api/pdfviewer/hasunsavedchanges) the moment the bytes leave the viewer — whether your upload of them succeeds is yours to track (see [Saving as the user works](https://kernstra.com/docs/web-sdk/guides/replace)).

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `config?` | `ExportConfig` | Overrides the viewer's [export](https://kernstra.com/docs/web-sdk/config/export) option for this exporter. |

Returns `PDFExporter`

Throws:

- `INVALID_STATE` after `destroy()`.

```ts
const exporter = viewer.createExporter();
await exporter.download('report.pdf');
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/createexporter

### `print`

```ts
print(options?: PrintOptions): Promise<void>
```

Ask for pages and annotations (or take them from `options`), render at 300 DPI (within [performance.maxRenderPixels](https://kernstra.com/docs/web-sdk/config/performance/maxrenderpixels)) with progress, and open the browser print dialog. Needs the `print` feature.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `options?` | `PrintOptions` | `{ pages?: number[]; annotations?: boolean }` — the pages to print (zero-based; every page when omitted) and whether annotations are printed (`true` when omitted). Without it the viewer asks for pages and annotations. |

Returns `Promise<void>`: Resolves when the browser reports the print dialog closed (`afterprint`), or after a minute where it never does.

Throws:

- `INVALID_ARGUMENT` for a `pages` list that is empty or names a page the document has not.
- `FEATURE_NOT_LICENSED` without the `print` feature.
- `PERMISSION_DENIED` when the document forbids printing.
- `INVALID_STATE` after `destroy()`.

```ts
await viewer.print({ pages: [0, 1], annotations: false });
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/print

## Events

### `on`

```ts
on<K extends keyof ViewerEventMap>(event: K, handler: (data: ViewerEventMap[K]) => void, options?: SubscribeOptions): () => void
```

Subscribe to a typed event. Document lifecycle events (`initialized`, `licenseChanged`, `documentLoaded`, `documentClosed`) are global and fire on `Kernstra.events`, not here; a handler registered on the wrong emitter never runs and never warns — see [EventEmitter](https://kernstra.com/docs/web-sdk/api/eventemitter).

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `event` | `keyof ViewerEventMap` | A per-viewer event name — see [Events](https://kernstra.com/docs/web-sdk/api/events). |
| `handler` | `(data: ViewerEventMap[K]) => void` | Receives the typed payload. |
| `options?` | `SubscribeOptions` | `once` hears the next emit only; `signal` unsubscribes when an `AbortSignal` aborts (a signal already aborted subscribes nothing). |

Returns `() => void`: The unsubscribe.

Throws:

- `INVALID_STATE` after `destroy()`.

```ts
const stop = viewer.on('pageChanged', ({ pageIndex }) => setCurrentPage(pageIndex));
stop();

const controller = new AbortController();
viewer.on('zoomChanged', ({ scale }) => setZoom(scale), { signal: controller.signal });
viewer.on('documentChanged', () => markDirty(), { signal: controller.signal });
controller.abort(); // both gone
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/on

### `off`

```ts
off<K extends keyof ViewerEventMap>(event: K, handler: (data: ViewerEventMap[K]) => void): void
```

Unsubscribe from an event, with the same handler reference.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `event` | `keyof ViewerEventMap` | The event name. |
| `handler` | `(data: ViewerEventMap[K]) => void` | The same handler reference that was passed to `on()`. |

Returns `void`

```ts
const onPage = (data: ViewerEventMap['pageChanged']) => setCurrentPage(data.pageIndex);
viewer.on('pageChanged', onPage);
viewer.off('pageChanged', onPage);
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/off

## Lifecycle

### `whenReady`

```ts
whenReady(): Promise<void>
```

Resolves once the viewer is ready: pages laid out, form fields and tools wired, the pages in view queued to paint — the moment to remove a loading indicator of your own. A progressive document's later pages still arrive after it ([doc.whenFullyLoaded()](https://kernstra.com/docs/web-sdk/api/pdfdocument/whenfullyloaded) says when every byte has).

- Kind: method
- Since: 1.0.0

Returns `Promise<void>`

Throws:

- `ABORTED` if `destroy()` comes first.
- `PAGE_ERROR` when the file's fields or pages cannot be wired (the viewer stands; the `error` event reports it too).

```ts
const viewer = new PDFViewer(container, doc);
await viewer.whenReady();
hideLoader();
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/whenready

### `destroy`

```ts
destroy(): void
```

Removes everything the viewer added to the page: its DOM, every document- and window-level listener, observers, timers, the render worker, its tools, and any of its dialogs or popovers still open (a pending `print()` resolves without printing). Safe to call twice. A shape or annotation still selected goes back into the document where it was. With the last viewer on the page it also removes the shared stylesheets. The Image tool's recent images, saved signatures and user stamps stay for the page session, for the next viewer.

**It does not close the underlying `PDFDocument`** — call [document.close()](https://kernstra.com/docs/web-sdk/api/pdfdocument/close) yourself, before or after, or you leak its memory. After it, every member that acts — `goToPage()`, `setScale()`, `activateTool()`, `print()`, `search()`, `openPanel()`, `undo()`, `createExporter()` and the rest — throws `INVALID_STATE` rather than doing nothing; the getters keep answering their last values. A stale handler that still reaches the viewer is a bug this makes visible.

- Kind: method
- Since: 1.0.0

Returns `void`

```ts
viewer.destroy();
await doc.close();
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/destroy

## Static

### `showLoadingIndicator`

```ts
static showLoadingIndicator(container: HTMLElement, options?: { loader?: LoaderConfig; locale?: LocaleConfig }): () => void
```

Show a loading spinner before a viewer exists. Involves no document. A loader put in the container stays over the viewer until you remove it: show the loader, open, construct, `await viewer.whenReady()`, hide.

- Kind: static method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `container` | `HTMLElement` | The element the viewer will be rendered into. |
| `options?` | `{ loader?: LoaderConfig; locale?: LocaleConfig }` | `options.loader` is a [LoaderConfig](https://kernstra.com/docs/web-sdk/config/loader) and `options.locale` a [LocaleConfig](https://kernstra.com/docs/web-sdk/config/locale) — pass the ones you give the viewer. |

Returns `() => void`: A cleanup function.

```ts
const hide = PDFViewer.showLoadingIndicator(container, { locale: { lang: 'de' } });
const doc = await PDFDocument.fromUrl('/report.pdf');
const viewer = new PDFViewer(container, doc, { locale: { lang: 'de' } });
await viewer.whenReady();
hide();
```

Page: https://kernstra.com/docs/web-sdk/api/pdfviewer/showloadingindicator

Web page: https://kernstra.com/docs/web-sdk/api/pdfviewer
