EmbedPDF

Annotations

Annotations are what people add on top of a page: highlights, sticky notes, shapes, stamps, free text, links. They’re stored in the PDF, so every PDF app shows them, and they stay separate from the page’s own content, so you can change or remove them later.

const page = doc.page(pageRef);
 
const { annotation } = await page.annotations.create({
  subtype: 'square',
  box: { x: 72, y: 92, width: 200, height: 100 },
  color: '#0078ff',
});
 
await page.annotations.update(annotation.ref, { contents: 'Check this table' });
await page.annotations.delete(annotation.ref);

Kinds#

Each kind has its own page with an example and its fields:

List annotations#

Every annotation in the document, or the ones on one page:

const { annotations } = await doc.annotations.list(); // every page
const { annotations } = await doc.annotations.list({ pages: [pageA, pageB] }); // some pages
const { annotations } = await doc.page(pageRef).annotations.list(); // one page

A list is cheap even for a whole document: the engine reads the annotations without loading the pages. Each annotation names its page, and the list’s pages has one entry per page it read.

Each annotation has a subtype (its kind), a rect (where it is on its page) and a ref (its address). Keep the ref: it’s how you change or delete the annotation later.

Positions are in points from the top-left corner of the page, with y going down, like every position in the engine: a box is { x, y, width, height } and a point { x, y }. See Coordinates.

for (const annotation of annotations) {
  console.log(annotation.subtype, annotation.rect, annotation.contents);
}

A list includes kinds the engine can’t create yet: they come back as subtype: 'unsupported', with their position and common fields. They stay in the PDF untouched.

Form widgets are annotations too, of kind 'widget', but they’re the form’s: a list here never holds one, and doc.forms.list() returns them with the fields. You place and change them through the form; see Creating form fields.

Fields every annotation has#

Besides subtype, rect and ref, every kind has these fields:

FieldWhat it is
contentsA comment, the text PDF apps show for the annotation.
subjectA short title PDF apps show with the comment, such as 'Question'.
nmA unique name on its page. See Refs & identity.
replyThe annotation it answers, or null. See Notes & replies.
popupThe ref of its popup window, or null. See Popups.
groupIdThe group it belongs to, when you work with groups.

Each annotation also has the flags and the fields that say who wrote it.

Add an annotation#

create() takes the kind and its fields, and returns the new annotation:

const { annotation } = await page.annotations.create({
  subtype: 'circle',
  box: { x: 60, y: 392, width: 120, height: 100 },
  interiorColor: '#1e90ff',
});

Fields you leave out get the kind’s default. The engine draws the annotation’s appearance for you, so it shows the same in every PDF app.

Stamps and file attachments also carry bytes (an image, a file). Pass those in a second argument; see Stamps and File attachments.

Change an annotation#

update() takes the ref and only the fields you want to change:

await page.annotations.update(annotation.ref, {
  color: '#dc143c',
  contents: 'Reviewed',
});

A field you leave out keeps its value. A field set to null is removed, for example { interiorColor: null } for no fill. A field’s value is replaced whole (a file, a list of points), except rich text’s body style, which merges.

You can also pass an annotation you read straight back: every field it has is one update() accepts. That makes “change a copy, then save it” code simple:

await page.annotations.update(annotation.ref, { ...annotation, opacity: 0.5 });

Fields the engine writes (author, the dates, userId, ref…) are accepted and ignored. For free text, send contents or richText, not both with different text.

Move or resize by rect#

An update takes a new rect for every annotation, as a script’s annot.rect does in Acrobat. A note, a file, a link, a popup and a redaction take it as it is.

Every other annotation is drawn from its shape: rectangles, ellipses, text boxes, stamps, carets and form fields’ widgets from their box, lines, polylines, polygons and ink from their points, and text markup from its quads. For these, a new rect puts the shape there: each point is mapped from the old rect onto the new one, each axis on its own. A rect of the same size moves the drawing, and a bigger one stretches it:

const { rect } = line;
 
// 40 points right, 20 down
await page.annotations.update(line.ref, {
  rect: { ...rect, x: rect.x + 40, y: rect.y + 20 },
});

The engine then works out rect again from the shape. After a resize it can differ a little from the one you sent: a line keeps its width, and its endings keep their size. The rect you read, sent back, changes nothing, so a read passed straight back works.

  • A new rect with a new shape (box, points or quads) fails with InvalidArg: send one or the other.
  • A turned drawing can only move. A rect of another size fails with InvalidArg; resize its box or its points instead. A box at a quarter turn is the exception: it stands upright on the page, so a rect resizes it along its own sides.
  • A rect that would squash a drawing to no width or no height fails with InvalidArg.

shapeForRect(annotation, rect) gives the fields that put an annotation at a rect, by the same rule. Use it to place a copy somewhere else:

import { shapeForRect } from '@embedpdf/engine';
 
await page.annotations.create({ ...copy, ...shapeForRect(copy, target) });

A create works out a drawn annotation’s rect from its shape, whatever rect you send, so a copy of another app’s drawing keeps its shape exactly. A box turned a quarter can be created by its rect instead of its box; see Turned shapes.

Delete and reorder#

await page.annotations.delete(annotation.ref);
 
await page.annotations.reorder([annotation.ref], 'start'); // to the back
await page.annotations.reorder([annotation.ref], { after: other.ref }); // just above another

Deleting an annotation also deletes its popup, if it has one, and its replies; see Delete a note.

Annotations on a page are drawn in order, so the last one is on top. reorder(refs, position) moves annotations together, in the order you pass them, right after or before another annotation of the page, or to the back ('start') or the front ('end'). It resolves the page’s new order, order, as refs, and fires annotations.reordered.

Widgets are drawn above every annotation, in an order of their own, so a widget is never a neighbour here: reorder() refuses one with InvalidArg. Restack widgets with doc.forms.reorderWidgets().

Style#

Most kinds share the same style fields:

FieldWhat it isDefault
colorLine color, such as '#1e90ff'per kind
interiorColorFill color of closed shapes, or null for no fillno fill
opacityFrom 0 (invisible) to 1 (solid)1
strokeWidthLine width in points1
borderStyle'solid' or 'dashed''solid'
dashArrayThe pattern a 'dashed' border draws, such as [3, 2][3]

A pattern alone doesn’t make a border dashed: a 'solid' border keeps its dashArray and draws solid. dashArray: null on a dashed border draws the default [3], 3 points on and 3 off. A border from another app can read 'beveled' or 'inset'; only form widgets draw those, so a write takes them only sent back unchanged.

Colors are '#rrggbb', as in CSS: the engine gives them in lowercase and takes either case. They never include transparency: that’s what opacity is for. The default color depends on the kind (yellow for a highlight, red for a shape, black for free text); each kind’s page lists its defaults.

A PDF can store a color as gray or CMYK. It reads as the same color in '#rrggbb', and sent back unchanged it stays as the file has it, so passing a read straight back rewrites nothing.

Who wrote it#

The engine records who created and last changed each annotation, from the user the document was opened for. You read these fields; you don’t set them:

FieldWhat it is
authorThe display name shown in PDF apps.
createdAtWhen it was created, as an ISO date.
modifiedAtWhen it was last changed.
userIdThe id of the user who owns it.
createdByThe id of the user who created it.
modifiedByThe id of the user who last changed it.
groupIdThe group it belongs to, when you work with groups.

The user comes from the identity you pass when you open the document; see Opening documents.

Flags#

Every annotation has the standard PDF flags as true/false fields. Set the ones you need; the rest stay off (on create) or keep their value (on update). The exception is print: a new annotation prints unless you set print: false, as in Acrobat. A popup doesn’t print.

await page.annotations.update(annotation.ref, { print: true, locked: true });
FlagWhat it does
printPrinted with the page. Off: shown on screen only.
hiddenNot shown and not printed.
noViewNot shown on screen (can still print).
lockedCan’t be moved, resized or deleted in PDF apps.
lockedContentsIts text can’t be changed in PDF apps.
readOnlyCan’t be changed at all in PDF apps.
noZoomKeeps its size when the page is zoomed (notes, icons).
noRotateStays upright when the page is rotated.
invisibleHidden when the app doesn’t know the annotation’s kind.
toggleNoViewFlips noView when the pointer is over it.

Flags are instructions for PDF apps. The engine itself still lets you change a locked annotation; the viewer’s annotation tools respect the flags in their interface.

Permissions#

Reading annotations needs doc.annotate.read. Creating, changing and deleting them needs a permission for that action, which can be limited to the user’s own annotations: annotations:update:self lets a user edit what they created and nothing else. See Permissions.

More#

Was this page helpful?

Your feedback goes directly to the documentation team.