AnnotationsApi
The Highlight, Note, Link and shape tools and the selection bar's text marks, headless: create() writes highlights, underlines, squiggly underlines, strikeouts, notes, links, squares, circles, lines and ink drawings; list() also reports stamps and free text. What list() reports — name, flags, colors, quads, strokes, line ends, end caps, stroke width, link target, dates — create() takes back, so text marks, notes, links and shapes copy between documents (under the extract feature: without it the words come back '', and a web link's uri '', which create() refuses); a line's cap fill and a dashed border do not come along. Stamps and text boxes are not created here: their picture is their appearance, which only their tools draw.
License. Creating, changing, deleting and flattening, replying and setting review status need the annotations feature; a form field's widget changed, removed or flattened needs forms too. Listing annotations and reading comments need no feature, their contents and author blanked to '' without extract. On a certified document, a write the certification forbids rejects with INVALID_STATE unless the call passes invalidateSignatures: true, the same escape as flattening a signed document: the certification is then broken for good and nothing is asked again.
Ids, not positions. An annotation is addressed by its id: its name when the file names it, which other readers keep across a round trip, else an id the SDK derives from its object in the file (obj:<number>), which a save keeps too. What the tools and create() write is named with a UUID. A file that names two annotations alike addresses the first in page order, everywhere. The tools honor an annotation's flags; the API does not. Erase and Note leave a Locked or ReadOnly annotation alone and do not show a Hidden one; doc.annotations is your code and acts on any. color is the color, opacity how opaque, each its own member: null means the annotation carries no color entry, and a reader uses its default.
Every annotation the SDK writes is a proper comment in other readers: it carries the print flag, a creation date and a modification date, and a subject naming what it is, in English from the API. A link is navigation and is written without them. A note also keeps its size and orientation when the page is zoomed or turned (noZoom, noRotate), as Acrobat writes notes. Form fields appear in list() as type: 'widget'; their value lives in the form environment, so read and fill those through FormsApi.
Every rect is a Rect in page space (PDF points, origin bottom-left, y upwards) and a text mark's lines are Quads, their corners named as Acrobat writes them. Colors go in as any CSS color string a PDF can hold (hex, rgb(), hsl(), named colors; oklch(), lab(), color() and color-mix() are refused with INVALID_ARGUMENT) and come out as lower-case #rrggbb with opacity beside them.
Usage
const comments = (await doc.annotations.list(0)).filter((a) => a.contents !== ''); await doc.annotations.create(0, { type: 'note', rect: { x: 72, y: 700, width: 20, height: 20 }, contents: 'Check this total', author: 'Accounts', });
Annotations
AnnotationInfo[]. An id is the annotation's name — a UUID for one the SDK created — or obj:<number> for one the file carries without a name. Without the extract feature contents, author and a web link's uri are ''. A link's uri is the address the viewer would follow (http:, https:, mailto:, tel:, as create() takes it — a bare host in the file reads with https:// in front); a link with any other scheme has uri and targetPage both null. created and modified are Dates, null where a file from another producer says nothing or writes something that is not a PDF date. Needs no feature.| Parameter | Type | Description |
|---|---|---|
| pageIndex | number | Zero-based page index. |
Promise<AnnotationInfo[]> Each with pageIndex, id, type, rect, contents, author, color, opacity, fillColor, created, modified, quads, inkList, vertices, line, lineEndings, strokeWidth, uri, targetPage, flags, inReplyTo and status.INVALID_ARGUMENT— a page index out of range.DOCUMENT_CLOSED— the document has been closed.
const links = (await doc.annotations.list(0)).filter((a) => a.type === 'link');
AnnotationInfo, or null — the way to keep hold of one across saves, deletions and other readers. A miss reads every page. Words blanked as list() blanks them without extract. Needs no feature.| Parameter | Type | Description |
|---|---|---|
| id | string | The annotation's id, as list() or create() gave it. |
Promise<AnnotationInfo | null> INVALID_ARGUMENT— anidthat is not a non-empty string.DOCUMENT_CLOSED— the document has been closed.
const note = await doc.annotations.get(id); if (note) console.log(note.pageIndex, note.contents);
Adds one. A text mark (highlight, underline, squiggly, strikeout) covers quads (omitted or empty: its rect, which becomes their bounds); a line needs line, an ink needs inkList, a polygon needs vertices (three or more corners), a polyline too (two or more). A square, circle, line, drawing, polygon or polyline is written as its tool writes it — the same properties and appearance, so lineEndings: { end: 'openArrow' } is the Arrow tool's arrow: a square's or circle's rect is the box given; a line's, drawing's, polygon's or polyline's is worked out from its geometry, and the one given is not used.
A shape with no color is drawn in #18181B, the default palette's first color; strokeWidth is up to 14 400 points, one point when omitted or null, and 0 gives a square, circle or polygon no outline (a line, drawing or polyline is drawn at one point for 0); lineEndings default to 'none' and are ignored on anything but a line; borderStyle is solid when omitted, dashed or dotted is a pattern sized to the stroke, and cloudy goes round a square, circle or polygon only (an open shape asked for it is drawn solid). An id of your own is written as its name — saved in the file for every reader, so no secrets — and must be one no annotation in the document carries, at most 256 characters, no control characters; omitted, it gets a UUID. flags (a Partial<AnnotationFlags>: a flag left out is off, and it prints unless noPrint) are written as given. A link goes to uri — http:, https:, mailto: or tel:, a bare address gets https://, stored percent-encoded — or to targetPage. Needs the annotations feature.
| Parameter | Type | Description |
|---|---|---|
| pageIndex | number | Zero-based page index. |
| annotation | NewAnnotation | type (one of highlight, underline, squiggly, strikeout, note, link, square, circle, line, ink, polygon, polyline) and rect are required; contents, author, color, opacity, fillColor, quads, line, inkList, vertices, strokeWidth, lineEndings, borderStyle, uri, targetPage, id, flags, created and modified are optional. |
| options? | SignedDocumentOptions | { invalidateSignatures? }. |
Promise<string> The new annotation's id.INVALID_ARGUMENT— a stamp, a free text, atypeit does not make (the message names the ones it does:'rectangle'is'square'), a missing line, strokes or corners, a shape point that is not a number or lies past ±14 400 points, a color string the API does not take, anopacityoutside 0–1, a stroke width outside 0 to 14 400, an end cap not inLineEnding, a border style not inBorderStyle, another link scheme, atargetPagethat is not a page of the document, apageIndexthat is not a page of the document, anidthat is empty, taken, longer than 256 characters, holds a control character or is of theobj:…form the SDK derives, a web link whoseuriis'', or text-mark quads with a coordinate that is not a number or lies past ±14 400 points, or that run along more than 1 440 000 points of line in all.PAGE_ERROR— the annotation could not be added.FEATURE_NOT_LICENSED— the license lacksannotations.DOCUMENT_CLOSED— the document has been closed.INVALID_STATE— a certified document whose certification forbids it, unlessoptions.invalidateSignatures; or a progressive document still downloading.
const id = await doc.annotations.create(0, { type: 'line', rect: { x: 0, y: 0, width: 0, height: 0 }, // worked out from the geometry line: { start: { x: 72, y: 600 }, end: { x: 300, y: 600 } }, lineEndings: { end: 'openArrow' }, // the Arrow tool's arrow color: '#D92D20', });
Changes rect, contents, author, color, fillColor (a square's, circle's or polygon's; refused on anything else) or opacity of the annotation with that id. A color with no alpha changes the color alone; one with an alpha, or an opacity beside it, sets the opacity too; null is refused, a color entry cannot be removed. A new rect moves and scales what the annotation draws — quads, strokes, line ends, a polygon's or polyline's corners — with it; a stamp's or text box's own picture is fitted to it.
update({ color }) rewrites the entry and rebuilds the appearance, so every reader draws the new color. A text box is refused with INVALID_ARGUMENT, since its color is its words'. A stamp accepts the call, but its picture is its own and keeps its colors: only the color entry and the opacity change. update() stamps the modification date when it changes something; it also acts on a reply or a status entry as on any annotation. Needs the annotations feature, and forms too for a form field's widget.
| Parameter | Type | Description |
|---|---|---|
| id | string | The annotation's id. |
| changes | AnnotationUpdate | { rect?, contents?, author?, color?, fillColor?, opacity? }. |
| options? | SignedDocumentOptions | { invalidateSignatures? }. |
Promise<void> INVALID_ARGUMENT— an id no annotation has, a text box's color,fillColoron anything but a square, circle or polygon, anullcolor, or a value the API does not take.PAGE_ERROR— the change could not be written (the color cannot be written).FEATURE_NOT_LICENSED— the license lacksannotations(orforms, for a form field's widget).DOCUMENT_CLOSED— the document has been closed.INVALID_STATE— a certified document whose certification forbids it, unlessoptions.invalidateSignatures; or a progressive document still downloading.
await doc.annotations.update(id, { contents: 'Checked — fine', color: '#12B76A' });
other) and every reply and review status that answers it, which may sit anywhere on the page; their ids are gone with them. Needs the annotations feature, and forms too for a form field's widget.| Parameter | Type | Description |
|---|---|---|
| id | string | The annotation's id. |
| options? | SignedDocumentOptions | { invalidateSignatures? }. |
Promise<void> INVALID_ARGUMENT— an id no annotation has.PAGE_ERROR— the annotation could not be removed.FEATURE_NOT_LICENSED— the license lacksannotations(orforms, for a form field's widget).DOCUMENT_CLOSED— the document has been closed.INVALID_STATE— a certified document whose certification forbids it, unlessoptions.invalidateSignatures; or a progressive document still downloading.
await doc.annotations.delete(id);
annotations feature, and forms too where a form field's widget is flattened.| Parameter | Type | Description |
|---|---|---|
| pageIndex? | number | Zero-based page index. Omit for the whole document. |
| options? | SignedDocumentOptions | { invalidateSignatures? }. |
Promise<void> INVALID_ARGUMENT— a page index out of range.FEATURE_NOT_LICENSED— the license lacksannotations(orforms, when a page to flatten carries form fields).INVALID_STATE— a signed document, unlessoptions.invalidateSignatures; or a progressive document still downloading.PAGE_ERROR— a page could not be flattened.DOCUMENT_CLOSED— the document has been closed.
await doc.annotations.flatten(); // the whole document
Comments
CommentThread[] (every page's, in page order, without pageIndex): each markup annotation with its replies, its reviewers' latest statuses and the latest status. Links, form fields, pop-ups and hidden comments are left out. Contents and authors are blank as list() blanks them without extract. Needs no feature.| Parameter | Type | Description |
|---|---|---|
| pageIndex? | number | Zero-based page index. Omit for every page's, in page order. |
Promise<CommentThread[]> Each { comment, replies, statuses, status }: the annotation commented on, its replies (replies to replies included, oldest first), each reviewer's latest status entry (oldest first) and the latest status any reviewer set, or null.INVALID_ARGUMENT— a page index out of range.DOCUMENT_CLOSED— the document has been closed.
for (const thread of await doc.annotations.comments()) { console.log(thread.comment.author, thread.replies.length, 'replies', thread.status); }
update(…, { contents }), remove it with delete(). Needs the annotations feature.| Parameter | Type | Description |
|---|---|---|
| id | string | The comment's id. |
| reply | NewReply | { contents, author? }; author omitted is ''. |
| options? | SignedDocumentOptions | { invalidateSignatures? }. |
Promise<string> The reply's id.INVALID_ARGUMENT— an id no annotation has, the annotation is not a comment (a link, a form field, a pop-up, a status entry), orcontentsis empty.PAGE_ERROR— the reply could not be written.FEATURE_NOT_LICENSED— the license lacksannotations.DOCUMENT_CLOSED— the document has been closed.INVALID_STATE— a certified document whose certification forbids it, unlessoptions.invalidateSignatures; or a progressive document still downloading.
const replyId = await doc.annotations.reply(id, { contents: 'Agreed', author: 'Ann' });
options.author) status on the comment with that id. The entry's text is Acrobat's "Accepted set by Ann", or the status alone without an author. update() and delete() act on a status entry as on any annotation, rewriting that reviewer's history. Needs the annotations feature.| Parameter | Type | Description |
|---|---|---|
| id | string | The comment's id. |
| status | ReviewStatus | 'accepted', 'rejected', 'cancelled', 'completed', or 'none' to clear theirs. |
| options? | StatusOptions | author (the reviewer; omitted, '') and invalidateSignatures. |
Promise<string> The status entry's id.INVALID_ARGUMENT— an id no annotation has, a status not inReviewStatus, or the annotation is not a comment.PAGE_ERROR— the status could not be written.FEATURE_NOT_LICENSED— the license lacksannotations.DOCUMENT_CLOSED— the document has been closed.INVALID_STATE— a certified document whose certification forbids it, unlessoptions.invalidateSignaturesbesideoptions.author; or a progressive document still downloading.
await doc.annotations.setStatus(id, 'accepted', { author: 'Ann' });
XFDF
The annotations and the form field values as XFDF — the interchange format Acrobat, Nutrient and Apryse read. Every text mark, note, shape, drawing, polygon, free text and stamp with its name, author, subject, dates, color, opacity, flags, contents, pop-up and geometry; replies (inreplyto) and review statuses (state); the fields' values (<fields>, nested by the dots in their names) as doc.forms.getValues() reads them. A stamp goes out by its name and box alone — its picture stays in the document; a member of an annotation group goes out as an annotation of its own. Dates go out as PDF date strings, coordinates are the page's own, colors #RRGGBB.
Names are the hinge: the SDK writes every annotation's own name as its name, matches by it on import, and reads Acrobat's own names back, so a round trip through a review backend neither duplicates nor loses a comment. Needs the extract feature (the file carries the document's words) and, for the annotations, annotations.
| Parameter | Type | Description |
|---|---|---|
| options? | XFDFExportOptions | annotations (default true), fields (default true), links (default false: a link has no name in most files and would be duplicated by a re-import), pages (zero-based; omitted, every page), href (the <f href> file name Acrobat shows). |
Promise<string> A UTF-8 XML string.INVALID_ARGUMENT—pagesis not an array of pages of the document.FEATURE_NOT_LICENSED— the license lacksextract, orannotationsfor the annotations.DOCUMENT_CLOSED— the document has been closed.
const xfdf = await doc.annotations.exportXFDF({ href: 'contract.pdf' }); await fetch('/reviews/' + id, { method: 'PUT', body: xfdf });
Applies an XFDF file: its annotations are written as create(), reply() and setStatus() write them, with the file's names, authors, subjects and dates (a freetext as the Text Box tool writes it, from its <defaultappearance>), and its <fields> go through doc.forms.setValues().
options.mode: 'merge' (default) — an entry whose name the document already has replaces that annotation in place, keeping the replies in the document the file does not bring back (unless the file moves it to another page, where a thread cannot follow: the old one goes with its thread), and every other entry is added; 'replace' — every annotation the file could describe (text marks, notes, shapes, drawings, polygons, free text, replies, statuses) is removed first, so the document ends up with the file's alone; stamps, links and form fields stay either way.
skipped reasons: unsupported (a stamp, caret, attachment, sound or redaction, a state that is not a review status, an unknown element), no_page, invalid (missing geometry or a refused value, or a name that belongs to a form field, link or pop-up), no_parent (a reply to nothing). flags are written as the file states them; a member of a group (replyType="group") becomes an annotation of its own; an empty attribute is read as none. A name the API cannot write (an obj:… id, one too long) gets a fresh UUID; dashes, icon and a pop-up's own place are read but not written. The license is checked for both halves before either is written, and one annotations change is announced at the end. Needs annotations when the file has annotations, forms when it has fields.
On a certified document the annotations go in only where its certification allows them ('annotations'): at 'forms' a file with annotations is refused with INVALID_STATE unless options.invalidateSignatures (pass annotations: false to take its field values alone), and at 'none' the field values are rejected as read_only.
| Parameter | Type | Description |
|---|---|---|
| xml | string | A well-formed XFDF document. |
| options? | XFDFImportOptions | mode ('merge', the default, or 'replace'), annotations and fields (default true; switch each half off) and invalidateSignatures. |
Promise<XFDFImportResult> imported and replaced ids, fields (what setValues reported, or null), and skipped — every entry not applied, with its element, page, name, a reason (unsupported, no_page, invalid, no_parent) and an English message.INVALID_ARGUMENT— text that is not a well-formed XFDF document, or amodethat is neither.FEATURE_NOT_LICENSED— the license lacksannotations(file with annotations) orforms(file with fields).INVALID_STATE— a certified document whose certification forbids the file's annotations, unlessoptions.invalidateSignatures; or a progressive document still downloading.DOCUMENT_CLOSED— the document has been closed.
const { imported, replaced, skipped, fields } = await doc.annotations.importXFDF( await (await fetch('/reviews/' + id)).text(), ); if (skipped.length > 0) console.warn('Not applied:', skipped);