---
title: "Annotation types"
description: "Every annotation you can create and the exact fields each one takes."
source: "https://www.embedpdf.com/docs/engine/core-concepts/annotation-types"
---

# Annotation types

This page lists every annotation you can create with `page.annotations.create(...)` and
what you pass for each. For reading, updating, styling, and flags, see
[Annotations](https://www.embedpdf.com/docs/engine/core-concepts/annotations).

> Coordinates are in PDF points with the origin at the bottom-left of the page (y goes
> up). A rectangle is `{ left, bottom, right, top }` and a point is `{ x, y }`.

Every annotation also accepts these optional base fields, so they're left out of the
per-type tables below:

- `contents` — a text note/comment.
- `flags` — PDF flags, e.g. `{ print: true }` (see [Annotations](https://www.embedpdf.com/docs/engine/core-concepts/annotations#flags)).
- `nm` — a stable name you want to assign.

Anything you omit falls back to its default (a 1pt solid red stroke, full opacity, no
fill).

> Any annotation kind the engine doesn't author yet is read back as `unsupported`. It's listed and
> preserved, but can't be created or have its type-specific fields edited.

## Text markup

`highlight`, `underline`, `squiggly`, and `strikeout` all work the same way: a color and
one or more `quadPoints` quads marking the text regions. A quad is four points
(`p1`–`p4`).

```ts
await page.annotations.create({
  subtype: 'highlight', // or 'underline' | 'squiggly' | 'strikeout'
  color: { r: 255, g: 215, b: 0 },
  opacity: 0.4,
  quadPoints: [
    {
      p1: { x: 72, y: 712 },
      p2: { x: 272, y: 712 },
      p3: { x: 72, y: 696 },
      p4: { x: 272, y: 696 },
    },
  ],
});
```

| Field        | What it is                      | Required | Default |
| :----------- | :------------------------------ | :------- | :------ |
| `quadPoints` | One quad per marked text region | yes      | —       |
| `color`      | Markup color `{ r, g, b }`      | no       | red     |
| `opacity`    | Transparency `0`–`1`            | no       | `1`     |

## Square and circle

`square` (rectangle) and `circle` (ellipse) are drawn inside a `rect`, with a stroke
(`color`) and an optional fill (`interiorColor`).

```ts
await page.annotations.create({
  subtype: 'circle', // or 'square'
  rect: { left: 60, bottom: 300, right: 180, top: 400 },
  color: { r: 0, g: 0, b: 139 },
  interiorColor: { r: 30, g: 144, b: 255 },
  strokeWidth: 2,
  borderStyle: 'solid',
  opacity: 0.5,
});
```

| Field             | What it is                                  | Required | Default   |
| :---------------- | :------------------------------------------ | :------- | :-------- |
| `rect`            | Bounding box `{ left, bottom, right, top }` | yes      | —         |
| `color`           | Stroke color                                | no       | red       |
| `interiorColor`   | Fill color, or `null` for none              | no       | none      |
| `strokeWidth`     | Border thickness                            | no       | `1`       |
| `borderStyle`     | `'solid'`/`'dashed'`/`'beveled'`/`'inset'`  | no       | `'solid'` |
| `dashArray`       | Dash pattern (with `'dashed'`)              | no       | —         |
| `opacity`         | Transparency `0`–`1`                        | no       | `1`       |
| `cloudyIntensity` | Cloudy-border strength (`0` = plain)        | no       | `0`       |
| `rectDifferences` | Inset of the shape from `rect` (see below)  | no       | none      |

## Polygon and polyline

Both take a `vertices` point list and a `rect`. A `polygon` is closed and can be filled;
a `polyline` is open and can have arrowheads (`lineEndings`).

```ts
// Polygon (closed, filled)
await page.annotations.create({
  subtype: 'polygon',
  rect: { left: 60, bottom: 450, right: 180, top: 550 },
  vertices: [
    { x: 70, y: 460 },
    { x: 170, y: 460 },
    { x: 120, y: 540 },
  ],
  color: { r: 0, g: 0, b: 139 },
  interiorColor: { r: 255, g: 215, b: 0 },
  strokeWidth: 2,
});

// Polyline (open, with arrowheads)
await page.annotations.create({
  subtype: 'polyline',
  rect: { left: 220, bottom: 450, right: 360, top: 550 },
  vertices: [
    { x: 230, y: 460 },
    { x: 290, y: 540 },
    { x: 350, y: 460 },
  ],
  color: { r: 220, g: 20, b: 60 },
  strokeWidth: 2,
  lineEndings: { start: 'open-arrow', end: 'closed-arrow' },
});
```

| Field             | What it is                                 | Required | Default   |
| :---------------- | :----------------------------------------- | :------- | :-------- |
| `vertices`        | Ordered points `{ x, y }`                  | yes      | —         |
| `rect`            | Bounding box                               | yes      | —         |
| `color`           | Stroke color                               | no       | red       |
| `interiorColor`   | Fill, or `null` (polygon)                  | no       | none      |
| `strokeWidth`     | Line thickness                             | no       | `1`       |
| `borderStyle`     | `'solid'`/`'dashed'`/`'beveled'`/`'inset'` | no       | `'solid'` |
| `dashArray`       | Dash pattern (with `'dashed'`)             | no       | —         |
| `opacity`         | Transparency `0`–`1`                       | no       | `1`       |
| `lineEndings`     | Arrowheads — **polyline only**             | no       | none      |
| `cloudyIntensity` | Cloudy border — **polygon only**           | no       | `0`       |

## Line

A single straight line between two points, with optional endings.

```ts
await page.annotations.create({
  subtype: 'line',
  rect: { left: 400, bottom: 450, right: 520, top: 550 },
  linePoints: { start: { x: 410, y: 460 }, end: { x: 510, y: 540 } },
  color: { r: 0, g: 128, b: 128 },
  strokeWidth: 2,
  lineEndings: { start: 'none', end: 'open-arrow' },
});
```

| Field         | What it is                                 | Required | Default   |
| :------------ | :----------------------------------------- | :------- | :-------- |
| `linePoints`  | `{ start: { x, y }, end: { x, y } }`       | yes      | —         |
| `rect`        | Bounding box                               | yes      | —         |
| `color`       | Stroke color                               | no       | red       |
| `strokeWidth` | Line thickness                             | no       | `1`       |
| `borderStyle` | `'solid'`/`'dashed'`/`'beveled'`/`'inset'` | no       | `'solid'` |
| `dashArray`   | Dash pattern (with `'dashed'`)             | no       | —         |
| `opacity`     | Transparency `0`–`1`                       | no       | `1`       |
| `lineEndings` | Endings at start/end                       | no       | none      |

## Ink

Freehand drawing. `inkList` is an array of strokes; each stroke is an array of points
(one pen path). Ink has a stroke but no fill.

```ts
await page.annotations.create({
  subtype: 'ink',
  rect: { left: 60, bottom: 60, right: 300, top: 200 },
  inkList: [
    [
      { x: 70, y: 80 },
      { x: 120, y: 180 },
      { x: 180, y: 90 },
    ], // stroke 1
    [
      { x: 200, y: 100 },
      { x: 260, y: 160 },
    ], // stroke 2
  ],
  color: { r: 220, g: 20, b: 60 },
  strokeWidth: 3,
});
```

| Field         | What it is                                 | Required | Default   |
| :------------ | :----------------------------------------- | :------- | :-------- |
| `inkList`     | Array of strokes; each an array of points  | yes      | —         |
| `rect`        | Box covering all strokes                   | yes      | —         |
| `color`       | Stroke color                               | no       | red       |
| `strokeWidth` | Pen thickness                              | no       | `1`       |
| `borderStyle` | `'solid'`/`'dashed'`/`'beveled'`/`'inset'` | no       | `'solid'` |
| `dashArray`   | Dash pattern (with `'dashed'`)             | no       | —         |
| `opacity`     | Transparency `0`–`1`                       | no       | `1`       |

## Free text and callout

A `free-text` annotation draws text directly on the page (a sticky label, a comment box,
or a callout that points at something). It's one type with two `intent` values:
`'free-text'` for a plain box and `'free-text-callout'` for a box with a leader line.

The colors are the part worth understanding:

- `color` — the **border color and the text color**. This is the one color you almost
  always set.
- `fontColor` — optional. Set it only when you want the **text a different color than the
  border**. Leave it out and the text just follows `color`.
- `interiorColor` — the **box background**. Leave it out (or pass `null`) for a
  transparent box.

```ts
// Plain text box
await page.annotations.create({
  subtype: 'free-text',
  intent: 'free-text',
  rect: { left: 60, bottom: 600, right: 260, top: 660 },
  fontFamily: 'helvetica',
  fontSize: 14,
  textAlign: 'center',
  contents: 'Please review this section',
  color: { r: 20, g: 40, b: 60 }, // border + text
  interiorColor: { r: 250, g: 250, b: 210 }, // pale yellow background
});

// Callout (box + leader line pointing at something)
await page.annotations.create({
  subtype: 'free-text',
  intent: 'free-text-callout',
  rect: { left: 280, bottom: 600, right: 480, top: 660 },
  fontFamily: 'times-roman',
  fontSize: 12,
  textAlign: 'left',
  contents: 'Look here',
  color: { r: 0, g: 0, b: 0 },
  fontColor: { r: 200, g: 0, b: 0 }, // red text, black border
  calloutLine: [
    { x: 265, y: 605 }, // the point being called out
    { x: 320, y: 630 }, // optional knee
    { x: 280, y: 640 }, // where the line meets the box
  ],
  lineEnding: 'open-arrow',
});
```

| Field             | What it is                                               | Required | Default                                    |
| :---------------- | :------------------------------------------------------- | :------- | :----------------------------------------- |
| `intent`          | `'free-text'` or `'free-text-callout'`                   | yes      | —                                          |
| `rect`            | Bounding box                                             | yes      | —                                          |
| `fontFamily`      | A standard font or a registered font key (see below)     | yes      | —                                          |
| `fontSize`        | Text size in points                                      | yes      | —                                          |
| `textAlign`       | `'left'`/`'center'`/`'right'`                            | yes      | —                                          |
| `contents`        | The text to show, plain — lines separated by `\r`        | no       | —                                          |
| `richText`        | Styled paragraphs and runs — see [Rich text](#rich-text) | no       | one plain paragraph per line of `contents` |
| `color`           | Border + default text color                              | no       | black                                      |
| `fontColor`       | Text color override                                      | no       | follows `color`                            |
| `interiorColor`   | Box background, or `null` for none                       | no       | none                                       |
| `opacity`         | Transparency `0`–`1`                                     | no       | `1`                                        |
| `strokeWidth`     | Border thickness                                         | no       | `1`                                        |
| `borderStyle`     | `'solid'`/`'dashed'`/`'beveled'`/`'inset'`               | no       | `'solid'`                                  |
| `dashArray`       | Dash pattern (with `'dashed'`)                           | no       | —                                          |
| `calloutLine`     | Leader line, 2 or 3 points — **callout only**            | no       | none                                       |
| `lineEnding`      | Arrowhead at the called-out end — **callout only**       | no       | none                                       |
| `rectDifferences` | Inset of the text box from `rect`                        | no       | none                                       |

The `calloutLine` is 2 points for a straight leader or 3 for a knee-jointed one. The
**first** point is what's being pointed at; the **last** point touches the text box.
`lineEnding` is the arrowhead drawn at the pointed-at end — same names as
[line endings](#line-endings).

### Fonts

`fontFamily` is one of the 14 standard PDF fonts, so they render everywhere without
embedding:

`courier`, `courier-bold`, `courier-bold-oblique`, `courier-oblique`, `helvetica`,
`helvetica-bold`, `helvetica-bold-oblique`, `helvetica-oblique`, `times-roman`,
`times-bold`, `times-bold-italic`, `times-italic`, `symbol`, `zapf-dingbats`.

`fontFamily` also accepts the key of a font you've registered with the engine — pass
`'noto-sc'` instead of a standard name to draw CJK, Cyrillic, or any other script, and a
glyph subset is embedded on save. An annotation set in a registered font reads `fontFamily`
back as that key on any engine the font is registered on (elsewhere, as the family name the
document carries). See [Custom fonts](https://www.embedpdf.com/docs/engine/core-concepts/custom-fonts).

### Rich text

Every free text reads back a `richText` document next to its plain `contents`, and you
can write one on create or update. It is the model Acrobat stores as `/RC` (XHTML) and
`/DS`, so what you author here is what Acrobat and every other viewer show, and what you
read is what they wrote:

- `body` — the annotation's default style: the face (`family`, `weight` `100`–`900`,
  `italic`), `size` in points, `color` as `#RRGGBB`, `decoration` (`'underline'`,
  `'line-through'`), `script` (`'normal'`, `'sub'`, `'super'`), `letterSpacing`,
  `horizontalScale`, and the paragraph defaults `align` (`'left'`, `'center'`, `'right'`,
  `'justify'`) and `dir` (`'ltr'`, `'rtl'`).
- `paragraphs` — each a list of `runs`. A run is `{ text, style? }` where `style` holds
  **only the properties that run overrides**. Runs are deltas over the body: change the
  body's size and every run that did not set its own size follows. A paragraph names
  `align`/`dir` only where it differs from the body. A `\r` inside a run's text is a hard
  line break inside the paragraph.

`contents` is always the plain projection of `richText` — paragraphs joined by `\r`, runs
concatenated — so a client that only knows plain text keeps working.

```ts
// Create with formatting: a bold word, a red word, a centred second line
// with a subscript.
await page.annotations.create({
  subtype: 'free-text',
  intent: 'free-text',
  rect: { left: 60, bottom: 600, right: 360, top: 660 },
  fontFamily: 'helvetica',
  fontSize: 12,
  textAlign: 'left',
  color: { r: 0, g: 0, b: 0 },
  richText: {
    body: { family: 'Helvetica', size: 18, color: '#102030' },
    paragraphs: [
      {
        runs: [
          { text: 'Hello ' },
          { text: 'bold', style: { weight: 700 } },
          { text: ' red', style: { color: '#FF0000' } },
        ],
      },
      { align: 'center', runs: [{ text: 'H' }, { text: '2', style: { script: 'sub' } }] },
    ],
  },
});
// contents reads back as 'Hello bold red\rH2'; fontSize as 18 (the body).
```

On a `body`, the face and size you give become the annotation's `fontFamily` and
`fontSize`; anything you leave out of a body takes the engine's defaults, so send the
whole body when you send one. Leave `body` out to keep the annotation's current one and
replace only the paragraphs — the shape a text editor commits:

```ts
// Restyle a word: paragraphs only, the body (and its alignment) stays.
await page.annotations.update(ref, {
  subtype: 'free-text',
  richText: {
    paragraphs: [{ runs: [{ text: 'Hello ' }, { text: 'world', style: { italic: true } }] }],
  },
});
```

The patch rules, in one place:

| You send                              | What happens                                                                                                                                           |
| :------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `richText`                            | Replaces the paragraphs, and the body when one is given.                                                                                               |
| `contents` alone                      | Rewrites the text as plain paragraphs in the body style. Run formatting is dropped by design — a plain-text client cannot preserve what it cannot see. |
| `fontFamily`, `fontSize`, `fontColor` | Move the **body**; runs keep their own overrides.                                                                                                      |
| `textAlign`                           | Moves the body's alignment (and `/Q`).                                                                                                                 |
| `contents` **and** `richText`         | Must agree (`contents` equal to the projection), else the update is refused with `InvalidArg` and nothing is written.                                  |

Run `family` names a face the way a PDF does — `'Helvetica'`, `'Times'`, `'Courier'`, a
registered font's family name (or its key), or a family the document already embeds. A
family that resolves nowhere substitutes Helvetica; the box still renders and edits. The
appearance stream is generated by the engine (shaped, line-broken and measured the way
Acrobat lays rich text out), so the saved file looks the same everywhere.

> Vertical alignment inside the box isn't authored: text is top-aligned, as in Acrobat.

## Caret

A `caret` marks a place in the text — typically where something should be inserted or
where an edit was made. It's the simplest annotation: just a position (`rect`), a color,
and an opacity.

```ts
await page.annotations.create({
  subtype: 'caret',
  rect: { left: 50, bottom: 700, right: 90, top: 730 },
  color: { r: 0, g: 0, b: 255 },
});
```

| Field             | What it is                           | Required | Default |
| :---------------- | :----------------------------------- | :------- | :------ |
| `rect`            | Where the caret sits                 | yes      | —       |
| `color`           | Caret color                          | no       | red     |
| `opacity`         | Transparency `0`–`1`                 | no       | `1`     |
| `rectDifferences` | Inset of the drawn caret from `rect` | no       | none    |

## Stamp

A `stamp` places an image — or a vector appearance — inside `rect`. You pass the bytes
inline as `source`; there is no separate upload or attachment step. The format is
detected from the bytes themselves (never from a declared mime type), and each format
takes the best path into the PDF:

- **PNG** — decoded natively, transparency preserved.
- **JPEG** — embedded as-is (no re-encoding).
- **Single-page PDF** — cloned in as a *vector* appearance that stays crisp at any zoom.

```ts
// From a file input, a fetch, or raw bytes — Blob and Uint8Array both work.
await page.annotations.create({
  subtype: 'stamp',
  rect: { left: 72, bottom: 640, right: 222, top: 715 },
  source: file, // PNG, JPEG, or single-page PDF bytes
  fit: 'contain',
});
```

| Field    | What it is                                                               | Required | Default     |
| :------- | :----------------------------------------------------------------------- | :------- | :---------- |
| `rect`   | Bounding box                                                             | yes      | —           |
| `source` | The content bytes: `Blob`, `Uint8Array`, or `{ data, mimeType?, name? }` | yes      | —           |
| `fit`    | How the content maps into `rect` (see below)                             | no       | `'contain'` |
| `name`   | Standard stamp label (`'Approved'`, `'Draft'`, `'Confidential'`, …)      | no       | none        |

`fit` uses the CSS `object-fit` vocabulary: `'contain'` preserves the aspect ratio and
keeps the content fully visible, `'cover'` preserves the aspect ratio and fills the box
(may crop), `'fill'` stretches to the box.

The bytes are written into the stamp's appearance stream, so the document stays fully
self-contained — it renders in any PDF viewer and survives download and re-open with no
side-car storage. The call is identical on the local and cloud engines.

Changing the geometry later (an `update` with a new `rect`, or a resize in the viewer)
re-fits the existing appearance — the image is never re-uploaded or re-encoded. Sending
a new `source` in an `update` replaces the content:

```ts
await page.annotations.update(ref, { subtype: 'stamp', source: otherImage });
```

Unsupported bytes (anything that isn't PNG, JPEG, or PDF) reject with `InvalidArg`
before any work happens.

## Cloudy borders

`square`, `circle`, and `polygon` support a cloudy (scalloped) border instead of a
straight one — the wavy outline reviewers often use to circle a region. Turn it on with
`cloudyIntensity`, a number that sets how pronounced the waves are:

- `0` (or omitted) — a plain, straight border.
- around `1` — gentle waves.
- around `2` — larger, more pronounced waves.

```ts
await page.annotations.create({
  subtype: 'square',
  rect: { left: 60, bottom: 300, right: 180, top: 400 },
  color: { r: 220, g: 20, b: 60 },
  strokeWidth: 2,
  cloudyIntensity: 2, // wavy "cloud" outline
});
```

Higher values mean bigger scallops. `cloudyIntensity` only affects the outline; the fill
(`interiorColor`) and everything else behave the same. It has no effect on `line`,
`polyline`, or text markup.

## Rectangle differences

`square`, `circle`, `free-text`, and `caret` accept an optional `rectDifferences` — the
gap, in points, between the `rect` you pass and where the shape (or text box) is actually
drawn. It's four non-negative insets, one per edge:

```ts
rectDifferences: { left: 6, top: 6, right: 6, bottom: 6 } // drawn 6pt inside rect on every side
```

You mostly need this with a cloudy border: the scallops bulge outward, so `rect` has to
be a little larger than the shape to fully contain them. `rectDifferences` records how far
the shape sits inside `rect` so the bounding box stays correct. For a plain border you can
leave it out — the shape just fills `rect`.

`polygon` doesn't use `rectDifferences`: its outline is already given exactly by
`vertices` (plus `rect`), so there's nothing to inset.

## Line endings

For `line` and `polyline`, `lineEndings` is `{ start, end }` where each is one of:

`none`, `square`, `circle`, `diamond`, `open-arrow`, `closed-arrow`, `butt`,
`r-open-arrow`, `r-closed-arrow`, `slash`.

The `r-` variants are reversed. Omit `lineEndings` to leave both ends plain.
