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.
structurePositionWhere it sits in the document’s tags (its logical structure), or null when it isn’t tagged. See Keyboard order.

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

Every kind but a link, a popup and a form widget also has richText: its comment with bold, italic or colored words, or null when the comment is plain text. contents is then the same text without the formatting. See Formatted comments. A free text draws its rich text in the box; see Free text.

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. Send contents or richText, not both with different text: a read sent back after the text changed fails with InvalidArg instead of putting the old text back.

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.

Change the kind#

Some kinds take the same fields, and an update turns one into another when you send a new subtype:

KindsWhat they draw
'highlight', 'underline', 'squiggly', 'strikeout'Marks over text
'square', 'circle'A rectangle or an ellipse inside box
await page.annotations.update(highlight.ref, { subtype: 'squiggly' });

It stays the same annotation: its ref, nm, place in the drawing order, replies, popup and group, and who wrote it and when. modifiedAt and modifiedBy change, as on any update. The engine draws it as its new kind, and annotations.updated carries the annotation as it now reads. Undoing the update turns it back.

The update takes the new kind’s fields, so other fields can change with it:

await page.annotations.update(highlight.ref, { subtype: 'underline', color: '#1d4ed8' });

Other kinds keep their subtype: a highlight can’t become ink, and a line can’t become a polyline, because they keep their shapes in different fields. Create a new annotation for those. A subtype the annotation can’t become fails with InvalidArg, and the message lists the kinds it can become.

A value the new kind has no field for is never dropped. The update fails with InvalidArg naming the field, and you clear it first, in the same change, so both writes are one step to undo. The one such value today is the intent of a strikeout that is part of a replacement:

await doc.apply({
  ops: [
    { type: 'annotations.update', ref: strikeout.ref, patch: { intent: null } },
    { type: 'annotations.update', ref: strikeout.ref, patch: { subtype: 'highlight' } },
  ],
});

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, 3]

A pattern alone doesn’t make a border dashed: a 'solid' border keeps its dashArray and draws solid. A dashed border whose dashArray is left out gets the default [3, 3], 3 points on and 3 off, written into the file, because Acrobat redraws a dashed border whose file names no pattern as a solid line. dashArray: null is no pattern: the file gets none and the border reads back null; the engine draws it with the default, as ISO 32000 says, and Acrobat draws it solid. A border from another app that names no pattern reads the same way. A pattern needs at least one length, so [] is refused: an empty pattern is a solid line, which is borderStyle: 'solid', and a border from another app with an empty pattern reads 'solid'. A border from another app can also 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.

On a shape, a line or ink, color: null draws no line at all, and strokeWidth keeps its value: see Without a line.

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.