# AnnotationsApi

The Highlight, Note, Link and shape tools and the selection bar's text marks, headless: `create()` writes highlights, underlines, squiggly underlines, strikeouts, notes, links, squares, circles, lines and ink drawings; `list()` also reports stamps and free text. What `list()` reports — name, flags, colors, quads, strokes, line ends, end caps, stroke width, link target, dates — `create()` takes back, so text marks, notes, links and shapes copy between documents (under the `extract` feature: without it the words come back `''`, and a web link's `uri` `''`, which `create()` refuses); a line's cap fill and a dashed border do not come along. Stamps and text boxes are not created here: their picture is their appearance, which only their tools draw.

**License.** Creating, changing, deleting and flattening, replying and setting review status need the `annotations` feature; a form field's widget changed, removed or flattened needs `forms` too. Listing annotations and reading comments need no feature, their `contents` and `author` blanked to `''` without `extract`. On a certified document, a write the certification forbids rejects with `INVALID_STATE` unless the call passes `invalidateSignatures: true`, the same escape as flattening a signed document: the certification is then broken for good and nothing is asked again.

**Ids, not positions.** An annotation is addressed by its `id`: its name when the file names it, which other readers keep across a round trip, else an id the SDK derives from its object in the file (`obj:<number>`), which a save keeps too. What the tools and `create()` write is named with a UUID. A file that names two annotations alike addresses the first in page order, everywhere. **The tools honor an annotation's flags; the API does not.** Erase and Note leave a Locked or ReadOnly annotation alone and do not show a Hidden one; `doc.annotations` is your code and acts on any. **`color` is the color, `opacity` how opaque**, each its own member: `null` means the annotation carries no color entry, and a reader uses its default.

Every annotation the SDK writes is a proper comment in other readers: it carries the print flag, a creation date and a modification date, and a subject naming what it is, in English from the API. A link is navigation and is written without them. A note also keeps its size and orientation when the page is zoomed or turned (`noZoom`, `noRotate`), as Acrobat writes notes. Form fields appear in `list()` as `type: 'widget'`; their value lives in the form environment, so read and fill those through [`FormsApi`](https://kernstra.com/docs/web-sdk/api/doc-forms).

Every `rect` is a [`Rect`](https://kernstra.com/docs/web-sdk/api/types#ty-rect) in page space (PDF points, origin bottom-left, `y` upwards) and a text mark's lines are `Quad`s, their corners named as Acrobat writes them. Colors go in as any CSS color string a PDF can hold (hex, `rgb()`, `hsl()`, named colors; `oklch()`, `lab()`, `color()` and `color-mix()` are refused with `INVALID_ARGUMENT`) and come out as lower-case `#rrggbb` with `opacity` beside them.

- Kind: namespace
- Access: `doc.annotations`

## Usage

```ts
const comments = (await doc.annotations.list(0)).filter((a) => a.contents !== '');

await doc.annotations.create(0, {
  type: 'note',
  rect: { x: 72, y: 700, width: 20, height: 20 },
  contents: 'Check this total',
  author: 'Accounts',
});
```

## Annotations

### `list`

```ts
list(pageIndex: number): Promise<AnnotationInfo[]>
```

Every annotation on a page as `AnnotationInfo[]`. An `id` is the annotation's name — a UUID for one the SDK created — or `obj:<number>` for one the file carries without a name. Without the `extract` feature `contents`, `author` and a web link's `uri` are `''`. A link's `uri` is the address the viewer would follow (`http:`, `https:`, `mailto:`, `tel:`, as `create()` takes it — a bare host in the file reads with `https://` in front); a link with any other scheme has `uri` and `targetPage` both `null`. `created` and `modified` are `Date`s, `null` where a file from another producer says nothing or writes something that is not a PDF date. Needs no feature.

- Kind: method
- Since: 1.0.0

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

Returns `Promise<AnnotationInfo[]>`: Each with `pageIndex`, `id`, `type`, `rect`, `contents`, `author`, `color`, `opacity`, `fillColor`, `created`, `modified`, `quads`, `inkList`, `vertices`, `line`, `lineEndings`, `strokeWidth`, `uri`, `targetPage`, `flags`, `inReplyTo` and `status`.

Throws:

- `INVALID_ARGUMENT` — a page index out of range.
- `DOCUMENT_CLOSED` — the document has been closed.

```ts
const links = (await doc.annotations.list(0)).filter((a) => a.type === 'link');
```

Page: https://kernstra.com/docs/web-sdk/api/doc-annotations/list

### `get`

```ts
get(id: string): Promise<AnnotationInfo | null>
```

The annotation with that id, wherever it sits now, as `AnnotationInfo`, or `null` — the way to keep hold of one across saves, deletions and other readers. A miss reads every page. Words blanked as `list()` blanks them without `extract`. Needs no feature.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | `string` | The annotation's id, as `list()` or `create()` gave it. |

Returns `Promise<AnnotationInfo | null>`

Throws:

- `INVALID_ARGUMENT` — an `id` that is not a non-empty string.
- `DOCUMENT_CLOSED` — the document has been closed.

```ts
const note = await doc.annotations.get(id);
if (note) console.log(note.pageIndex, note.contents);
```

Page: https://kernstra.com/docs/web-sdk/api/doc-annotations/get

### `create`

```ts
create(pageIndex: number, annotation: NewAnnotation, options?: SignedDocumentOptions): Promise<string>
```

Adds one. A text mark (`highlight`, `underline`, `squiggly`, `strikeout`) covers `quads` (omitted or empty: its `rect`, which becomes their bounds); a `line` needs `line`, an `ink` needs `inkList`, a `polygon` needs `vertices` (three or more corners), a `polyline` too (two or more). A square, circle, line, drawing, polygon or polyline is written as its tool writes it — the same properties and appearance, so `lineEndings: { end: 'openArrow' }` is the Arrow tool's arrow: a square's or circle's `rect` is the box given; a line's, drawing's, polygon's or polyline's is worked out from its geometry, and the one given is not used.

A shape with no `color` is drawn in `#18181B`, the default palette's first color; `strokeWidth` is up to 14 400 points, one point when omitted or `null`, and `0` gives a square, circle or polygon no outline (a line, drawing or polyline is drawn at one point for `0`); `lineEndings` default to `'none'` and are ignored on anything but a line; `borderStyle` is `solid` when omitted, `dashed` or `dotted` is a pattern sized to the stroke, and `cloudy` goes round a square, circle or polygon only (an open shape asked for it is drawn solid). An `id` of your own is written as its name — saved in the file for every reader, so no secrets — and must be one no annotation in the document carries, at most 256 characters, no control characters; omitted, it gets a UUID. `flags` (a `Partial<AnnotationFlags>`: a flag left out is off, and it prints unless `noPrint`) are written as given. A link goes to `uri` — `http:`, `https:`, `mailto:` or `tel:`, a bare address gets `https://`, stored percent-encoded — or to `targetPage`. Needs the `annotations` feature.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `pageIndex` | `number` | Zero-based page index. |
| `annotation` | `NewAnnotation` | `type` (one of `highlight`, `underline`, `squiggly`, `strikeout`, `note`, `link`, `square`, `circle`, `line`, `ink`, `polygon`, `polyline`) and `rect` are required; `contents`, `author`, `color`, `opacity`, `fillColor`, `quads`, `line`, `inkList`, `vertices`, `strokeWidth`, `lineEndings`, `borderStyle`, `uri`, `targetPage`, `id`, `flags`, `created` and `modified` are optional. |
| `options?` | `SignedDocumentOptions` | `{ invalidateSignatures? }`. |

Returns `Promise<string>`: The new annotation's id.

Throws:

- `INVALID_ARGUMENT` — a stamp, a free text, a `type` it does not make (the message names the ones it does: `'rectangle'` is `'square'`), a missing line, strokes or corners, a shape point that is not a number or lies past ±14 400 points, a color string the API does not take, an `opacity` outside 0–1, a stroke width outside 0 to 14 400, an end cap not in `LineEnding`, a border style not in `BorderStyle`, another link scheme, a `targetPage` that is not a page of the document, a `pageIndex` that is not a page of the document, an `id` that is empty, taken, longer than 256 characters, holds a control character or is of the `obj:…` form the SDK derives, a web link whose `uri` is `''`, or text-mark quads with a coordinate that is not a number or lies past ±14 400 points, or that run along more than 1 440 000 points of line in all.
- `PAGE_ERROR` — the annotation could not be added.
- `FEATURE_NOT_LICENSED` — the license lacks `annotations`.
- `DOCUMENT_CLOSED` — the document has been closed.
- `INVALID_STATE` — a certified document whose certification forbids it, unless `options.invalidateSignatures`; or a progressive document still downloading.

```ts
const id = await doc.annotations.create(0, {
  type: 'line',
  rect: { x: 0, y: 0, width: 0, height: 0 }, // worked out from the geometry
  line: { start: { x: 72, y: 600 }, end: { x: 300, y: 600 } },
  lineEndings: { end: 'openArrow' }, // the Arrow tool's arrow
  color: '#D92D20',
});
```

Page: https://kernstra.com/docs/web-sdk/api/doc-annotations/create

### `update`

```ts
update(id: string, changes: AnnotationUpdate, options?: SignedDocumentOptions): Promise<void>
```

Changes `rect`, `contents`, `author`, `color`, `fillColor` (a square's, circle's or polygon's; refused on anything else) or `opacity` of the annotation with that id. A `color` with no alpha changes the color alone; one with an alpha, or an `opacity` beside it, sets the opacity too; `null` is refused, a color entry cannot be removed. A new `rect` moves and scales what the annotation draws — quads, strokes, line ends, a polygon's or polyline's corners — with it; a stamp's or text box's own picture is fitted to it.

`update({ color })` rewrites the entry and rebuilds the appearance, so every reader draws the new color. A text box is refused with `INVALID_ARGUMENT`, since its color is its words'. A stamp accepts the call, but its picture is its own and keeps its colors: only the color entry and the opacity change. `update()` stamps the modification date when it changes something; it also acts on a reply or a status entry as on any annotation. Needs the `annotations` feature, and `forms` too for a form field's widget.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | `string` | The annotation's id. |
| `changes` | `AnnotationUpdate` | `{ rect?, contents?, author?, color?, fillColor?, opacity? }`. |
| `options?` | `SignedDocumentOptions` | `{ invalidateSignatures? }`. |

Returns `Promise<void>`

Throws:

- `INVALID_ARGUMENT` — an id no annotation has, a text box's color, `fillColor` on anything but a square, circle or polygon, a `null` color, or a value the API does not take.
- `PAGE_ERROR` — the change could not be written (the color cannot be written).
- `FEATURE_NOT_LICENSED` — the license lacks `annotations` (or `forms`, for a form field's widget).
- `DOCUMENT_CLOSED` — the document has been closed.
- `INVALID_STATE` — a certified document whose certification forbids it, unless `options.invalidateSignatures`; or a progressive document still downloading.

```ts
await doc.annotations.update(id, { contents: 'Checked — fine', color: '#12B76A' });
```

Page: https://kernstra.com/docs/web-sdk/api/doc-annotations/update

### `delete`

```ts
delete(id: string, options?: SignedDocumentOptions): Promise<void>
```

Removes the annotation with that id, with its pop-up (listed as `other`) and every reply and review status that answers it, which may sit anywhere on the page; their ids are gone with them. Needs the `annotations` feature, and `forms` too for a form field's widget.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | `string` | The annotation's id. |
| `options?` | `SignedDocumentOptions` | `{ invalidateSignatures? }`. |

Returns `Promise<void>`

Throws:

- `INVALID_ARGUMENT` — an id no annotation has.
- `PAGE_ERROR` — the annotation could not be removed.
- `FEATURE_NOT_LICENSED` — the license lacks `annotations` (or `forms`, for a form field's widget).
- `DOCUMENT_CLOSED` — the document has been closed.
- `INVALID_STATE` — a certified document whose certification forbids it, unless `options.invalidateSignatures`; or a progressive document still downloading.

```ts
await doc.annotations.delete(id);
```

Page: https://kernstra.com/docs/web-sdk/api/doc-annotations/delete

### `flatten`

```ts
flatten(pageIndex?: number, options?: SignedDocumentOptions): Promise<void>
```

Burns annotations into page content as they show on screen (Acrobat's default), dropping hidden ones. Omit the index for the whole document. **Not reversible.** Flattening on export keeps the page as it prints instead: what is marked not to print is dropped. Flattening a signed document changes what the signature covers, so it would no longer verify: refused as Acrobat refuses. Needs the `annotations` feature, and `forms` too where a form field's widget is flattened.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `pageIndex?` | `number` | Zero-based page index. Omit for the whole document. |
| `options?` | `SignedDocumentOptions` | `{ invalidateSignatures? }`. |

Returns `Promise<void>`

Throws:

- `INVALID_ARGUMENT` — a page index out of range.
- `FEATURE_NOT_LICENSED` — the license lacks `annotations` (or `forms`, when a page to flatten carries form fields).
- `INVALID_STATE` — a signed document, unless `options.invalidateSignatures`; or a progressive document still downloading.
- `PAGE_ERROR` — a page could not be flattened.
- `DOCUMENT_CLOSED` — the document has been closed.

```ts
await doc.annotations.flatten(); // the whole document
```

Page: https://kernstra.com/docs/web-sdk/api/doc-annotations/flatten

## Comments

### `comments`

```ts
comments(pageIndex?: number): Promise<CommentThread[]>
```

The page's comments as `CommentThread[]` (every page's, in page order, without `pageIndex`): each markup annotation with its replies, its reviewers' latest statuses and the latest status. Links, form fields, pop-ups and hidden comments are left out. Contents and authors are blank as `list()` blanks them without `extract`. Needs no feature.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `pageIndex?` | `number` | Zero-based page index. Omit for every page's, in page order. |

Returns `Promise<CommentThread[]>`: Each `{ comment, replies, statuses, status }`: the annotation commented on, its replies (replies to replies included, oldest first), each reviewer's latest status entry (oldest first) and the latest status any reviewer set, or `null`.

Throws:

- `INVALID_ARGUMENT` — a page index out of range.
- `DOCUMENT_CLOSED` — the document has been closed.

```ts
for (const thread of await doc.annotations.comments()) {
  console.log(thread.comment.author, thread.replies.length, 'replies', thread.status);
}
```

Page: https://kernstra.com/docs/web-sdk/api/doc-annotations/comments

### `reply`

```ts
reply(id: string, reply: NewReply, options?: SignedDocumentOptions): Promise<string>
```

Writes a reply to the comment with that id. Edit a reply with `update(…, { contents })`, remove it with `delete()`. Needs the `annotations` feature.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | `string` | The comment's id. |
| `reply` | `NewReply` | `{ contents, author? }`; `author` omitted is `''`. |
| `options?` | `SignedDocumentOptions` | `{ invalidateSignatures? }`. |

Returns `Promise<string>`: The reply's id.

Throws:

- `INVALID_ARGUMENT` — an id no annotation has, the annotation is not a comment (a link, a form field, a pop-up, a status entry), or `contents` is empty.
- `PAGE_ERROR` — the reply could not be written.
- `FEATURE_NOT_LICENSED` — the license lacks `annotations`.
- `DOCUMENT_CLOSED` — the document has been closed.
- `INVALID_STATE` — a certified document whose certification forbids it, unless `options.invalidateSignatures`; or a progressive document still downloading.

```ts
const replyId = await doc.annotations.reply(id, { contents: 'Agreed', author: 'Ann' });
```

Page: https://kernstra.com/docs/web-sdk/api/doc-annotations/reply

### `setStatus`

```ts
setStatus(id: string, status: ReviewStatus, options?: StatusOptions): Promise<string>
```

Sets a reviewer's (`options.author`) status on the comment with that id. The entry's text is Acrobat's "Accepted set by Ann", or the status alone without an author. `update()` and `delete()` act on a status entry as on any annotation, rewriting that reviewer's history. Needs the `annotations` feature.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | `string` | The comment's id. |
| `status` | `ReviewStatus` | `'accepted'`, `'rejected'`, `'cancelled'`, `'completed'`, or `'none'` to clear theirs. |
| `options?` | `StatusOptions` | `author` (the reviewer; omitted, `''`) and `invalidateSignatures`. |

Returns `Promise<string>`: The status entry's id.

Throws:

- `INVALID_ARGUMENT` — an id no annotation has, a status not in `ReviewStatus`, or the annotation is not a comment.
- `PAGE_ERROR` — the status could not be written.
- `FEATURE_NOT_LICENSED` — the license lacks `annotations`.
- `DOCUMENT_CLOSED` — the document has been closed.
- `INVALID_STATE` — a certified document whose certification forbids it, unless `options.invalidateSignatures` beside `options.author`; or a progressive document still downloading.

```ts
await doc.annotations.setStatus(id, 'accepted', { author: 'Ann' });
```

Page: https://kernstra.com/docs/web-sdk/api/doc-annotations/setstatus

## XFDF

### `exportXFDF`

```ts
exportXFDF(options?: XFDFExportOptions): Promise<string>
```

The annotations and the form field values as XFDF — the interchange format Acrobat, Nutrient and Apryse read. Every text mark, note, shape, drawing, polygon, free text and stamp with its name, author, subject, dates, color, opacity, flags, contents, pop-up and geometry; replies (`inreplyto`) and review statuses (`state`); the fields' values (`<fields>`, nested by the dots in their names) as `doc.forms.getValues()` reads them. A stamp goes out by its name and box alone — its picture stays in the document; a member of an annotation group goes out as an annotation of its own. Dates go out as PDF date strings, coordinates are the page's own, colors `#RRGGBB`.

Names are the hinge: the SDK writes every annotation's own name as its `name`, matches by it on import, and reads Acrobat's own names back, so a round trip through a review backend neither duplicates nor loses a comment. Needs the `extract` feature (the file carries the document's words) and, for the annotations, `annotations`.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `options?` | `XFDFExportOptions` | `annotations` (default `true`), `fields` (default `true`), `links` (default `false`: a link has no name in most files and would be duplicated by a re-import), `pages` (zero-based; omitted, every page), `href` (the `<f href>` file name Acrobat shows). |

Returns `Promise<string>`: A UTF-8 XML string.

Throws:

- `INVALID_ARGUMENT` — `pages` is not an array of pages of the document.
- `FEATURE_NOT_LICENSED` — the license lacks `extract`, or `annotations` for the annotations.
- `DOCUMENT_CLOSED` — the document has been closed.

```ts
const xfdf = await doc.annotations.exportXFDF({ href: 'contract.pdf' });
await fetch('/reviews/' + id, { method: 'PUT', body: xfdf });
```

Page: https://kernstra.com/docs/web-sdk/api/doc-annotations/exportxfdf

### `importXFDF`

```ts
importXFDF(xml: string, options?: XFDFImportOptions): Promise<XFDFImportResult>
```

Applies an XFDF file: its annotations are written as `create()`, `reply()` and `setStatus()` write them, with the file's names, authors, subjects and dates (a `freetext` as the Text Box tool writes it, from its `<defaultappearance>`), and its `<fields>` go through `doc.forms.setValues()`.

`options.mode`: `'merge'` (default) — an entry whose `name` the document already has replaces that annotation in place, keeping the replies in the document the file does not bring back (unless the file moves it to another page, where a thread cannot follow: the old one goes with its thread), and every other entry is added; `'replace'` — every annotation the file could describe (text marks, notes, shapes, drawings, polygons, free text, replies, statuses) is removed first, so the document ends up with the file's alone; stamps, links and form fields stay either way.

`skipped` reasons: `unsupported` (a stamp, caret, attachment, sound or redaction, a state that is not a review status, an unknown element), `no_page`, `invalid` (missing geometry or a refused value, or a name that belongs to a form field, link or pop-up), `no_parent` (a reply to nothing). `flags` are written as the file states them; a member of a group (`replyType="group"`) becomes an annotation of its own; an empty attribute is read as none. A name the API cannot write (an `obj:…` id, one too long) gets a fresh UUID; `dashes`, `icon` and a pop-up's own place are read but not written. The license is checked for both halves before either is written, and one `annotations` change is announced at the end. Needs `annotations` when the file has annotations, `forms` when it has fields.

On a certified document the annotations go in only where its certification allows them (`'annotations'`): at `'forms'` a file with annotations is refused with `INVALID_STATE` unless `options.invalidateSignatures` (pass `annotations: false` to take its field values alone), and at `'none'` the field values are rejected as `read_only`.

- Kind: method
- Since: 1.0.0

| Parameter | Type | Description |
| --- | --- | --- |
| `xml` | `string` | A well-formed XFDF document. |
| `options?` | `XFDFImportOptions` | `mode` (`'merge'`, the default, or `'replace'`), `annotations` and `fields` (default `true`; switch each half off) and `invalidateSignatures`. |

Returns `Promise<XFDFImportResult>`: `imported` and `replaced` ids, `fields` (what `setValues` reported, or `null`), and `skipped` — every entry not applied, with its `element`, `page`, `name`, a `reason` (`unsupported`, `no_page`, `invalid`, `no_parent`) and an English `message`.

Throws:

- `INVALID_ARGUMENT` — text that is not a well-formed XFDF document, or a `mode` that is neither.
- `FEATURE_NOT_LICENSED` — the license lacks `annotations` (file with annotations) or `forms` (file with fields).
- `INVALID_STATE` — a certified document whose certification forbids the file's annotations, unless `options.invalidateSignatures`; or a progressive document still downloading.
- `DOCUMENT_CLOSED` — the document has been closed.

```ts
const { imported, replaced, skipped, fields } = await doc.annotations.importXFDF(
  await (await fetch('/reviews/' + id)).text(),
);
if (skipped.length > 0) console.warn('Not applied:', skipped);
```

Page: https://kernstra.com/docs/web-sdk/api/doc-annotations/importxfdf

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