EmbedPDF

Notes & replies

A note is the sticky-note icon PDF apps show on a page, with a comment in it. Any annotation can have replies, so a note, a highlight or a shape can each start a conversation.

const { annotation: note } = await page.annotations.create({
  subtype: 'text',
  rect: { x: 500, y: 72, width: 20, height: 20 },
  contents: 'Is this total right?',
});
 
await page.annotations.create({
  subtype: 'text',
  rect: note.rect,
  contents: 'Yes, checked against the invoice.',
  reply: { to: note.ref },
});

A note’s subtype is 'text'.

Add a note#

FieldWhat it isDefault
rectThe icon’s box, which the icon fills. Required; see The note icon.
contentsThe comment.none
icon'note', 'comment', 'key', 'help', 'insert', 'paragraph' or 'new-paragraph''note'
colorThe icon’s color, '#rrggbb'.yellow
openWhether PDF apps show its comment window open; see Popups.false
opacityFrom 0 to 1.1
subjectA short label for the comment, such as 'Question'.none

Change the comment like any other field, and move the icon by its rect:

await page.annotations.update(note.ref, { contents: 'Is this total right? It looks low.' });
await page.annotations.update(note.ref, { rect: { x: 480, y: 72, width: 20, height: 20 } });

Reply to an annotation#

A reply is an annotation with a reply field that names the one it answers. It’s usually a note placed where its parent is. Here a note answers a highlight:

const { x, y } = highlight.rect;
 
await page.annotations.create({
  subtype: 'text',
  rect: { x, y, width: 20, height: 20 },
  contents: 'Agreed, this needs a source.',
  reply: { to: highlight.ref },
});

A reply is on the same page as the annotation it answers; a to on another page fails with InvalidArg. You can reply to a reply: it joins the same thread.

To take a reply out of its thread, set reply: null. It stays in the document as an annotation of its own.

Read a thread#

Replies aren’t nested inside their parent. Each one is its own annotation in the page’s list, and its reply.to is the parent’s ref. To find a note’s direct replies, compare refs with annotationKey():

import { annotationKey } from '@embedpdf/engine';
 
const { annotations } = await page.annotations.list();
const key = annotationKey(note.ref);
 
const replies = annotations.filter((a) => a.reply && annotationKey(a.reply.to) === key);

For a comments panel, buildCommentThreads() does the whole job. It turns a list into threads, one per annotation that isn’t a reply:

import { buildCommentThreads } from '@embedpdf/engine';
 
const { annotations } = await page.annotations.list();
 
for (const thread of buildCommentThreads(annotations)) {
  showComment(thread.root);
  thread.replies.forEach(showReply); // oldest first, replies to replies included
}

Each thread has:

FieldWhat it is
rootThe annotation the thread is about.
repliesEvery reply below it, oldest first. Review states aren’t in it.
groupedPartsParts of the root, such as the strikeout of a suggested replacement (not replies).
reviewThe review states set on it (below).

Popups, links and form widgets are left out: they’re never a comment.

Review states#

A reviewer’s verdict, such as accepted or rejected, is a reply too: a note with a state. It isn’t a comment, so hide its icon:

await page.annotations.create({
  subtype: 'text',
  rect: note.rect,
  reply: { to: note.ref },
  state: 'accepted',
  hidden: true,
});

Each state belongs to a stateModel, which the engine sets from the state:

statestateModel
'accepted', 'rejected', 'cancelled', 'completed', 'none''review'
'marked', 'unmarked' (a personal checkmark)'marked'

Other PDF apps can define models and states of their own; they read back as written. To write one, give both state and stateModel: a state the engine doesn’t know, without a stateModel, fails with InvalidArg.

To change a verdict, add a new state reply rather than editing the old one. Each reviewer’s latest state counts, and buildCommentThreads() works that out for you:

thread.review.lastChange?.state; // the latest verdict, from anyone
thread.review.byReviewer; // each reviewer's latest, by user id (or author name)
thread.review.markedBy; // who has checked it off

Popups#

A popup is the window a PDF app opens to show an annotation’s comment. It’s an annotation of its own, subtype: 'popup', linked to the one it shows by parent:

const { annotation: popup } = await page.annotations.create({
  subtype: 'popup',
  parent: note.ref,
  rect: { x: 320, y: 92, width: 180, height: 100 },
  open: true,
});
FieldWhat it isDefault
parentThe annotation whose comment it shows. Required.
rectWhere the window is when it’s open. Required.
openWhether PDF apps show the window open.false

The engine keeps the link both ways: after this call, the note’s popup field is the popup’s ref. You never set popup yourself. An annotation has one popup: creating a second fails with InvalidArg naming the first. To replace it, delete it and create the new one; to move it, update its rect.

A popup has no text of its own: it shows its parent’s contents. To open or close it:

await page.annotations.update(popup.ref, { open: false });

A note and its popup hold one open: set it on either, and both change (the update’s meta.changed names both). A popup made without open takes its note’s. Other annotations have no open of their own; their popup’s is the only one.

You don’t need a popup to have a note. Add one when the document should say where the comment window goes. A note without one can still start open: PDF apps make the window themselves.

Delete a note#

Deleting a note deletes its whole thread, in one change: its replies (and their replies), its grouped parts, its review states and every popup. Deleting a reply deletes that reply and the replies under it. This is how Acrobat treats a thread too, and it keeps other viewers from showing a reply without its note.

const { meta } = await page.annotations.delete(note.ref);
// meta.changed names everything that went, the note first

It’s all or nothing: when the user may not delete one of them (someone else’s reply, for a user who may only change their own annotations), nothing is deleted and the call fails with Forbidden. error.details.refs names the ones in the way.

Permissions#

A reply, a state and a popup are annotations, so creating one needs permission to create annotations, and deleting one needs permission to delete it. A user who may only change their own annotations can still reply to anyone’s. See Permissions.

Good to know#

Groups are parts, not replies#

reply.type is 'reply' unless you say otherwise. 'group' makes an annotation part of another one, such as the strikeout of a suggested replacement (see Highlights & text markup). PDF apps show a group as one annotation, with the comment, author and date of the annotation its parts point to; the parts’ own comments are ignored.

The note icon#

The icon fills rect, as in every PDF app: a bigger rect draws a bigger icon, and a rect that isn’t square stretches it. 20 × 20 points is the usual size, the one PDF apps place a note at. The icon is filled with color and outlined in black or white, whichever stands out against it. Each icon draws its own symbol, and a name the engine doesn’t know draws 'note'.

How it’s stored#

For readers who know PDF: reply is /IRT with /RT, a popup is linked by the parent’s /Popup and the popup’s /Parent, open is /Open on the note and on its popup, and states are /State and /StateModel on a text annotation, as ISO 32000 describes them. So threads, states and popups made here show up in other PDF apps, and theirs show up here.

Was this page helpful?

Your feedback goes directly to the documentation team.