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.
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).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).
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).
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).
// 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.
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.
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 followscolor.interiorColor— the box background. Leave it out (or passnull) for a transparent box.
// 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 | 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.
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.
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,weight100–900,italic),sizein points,coloras#RRGGBB,decoration('underline','line-through'),script('normal','sub','super'),letterSpacing,horizontalScale, and the paragraph defaultsalign('left','center','right','justify') anddir('ltr','rtl').paragraphs— each a list ofruns. A run is{ text, style? }wherestyleholds 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 namesalign/dironly where it differs from the body. A\rinside 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.
// 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:
// 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.
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.
// 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:
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.
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:
rectDifferences: { left: 6, top: 6, right: 6, bottom: 6 } // drawn 6pt inside rect on every sideYou 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.
Your feedback goes directly to the documentation team.