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 | no | — |
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. See Custom fonts.
Vertical alignment and rich text aren’t authored yet — text is single-style and top-aligned in the box.
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.