EmbedPDF

Annotations in code

Everything people do with the pointer, your code can do too. useAnnotation() gives you the annotations on screen, and every change you make shows at once, before the engine has saved it:

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

Loading live preview…

import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import {
  AnnotationLayer,
  annotationPlugin,
  useAnnotation,
  useAnnotationList,
  useAnnotationState,
  type AnnotationRef,
} from '@embedpdf/react/annotation';
import { localEngine } from '@embedpdf/engine';

import './create-update.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), annotationPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// Create a rectangle, then change, move and delete it, all from code.
function SquareControls() {
  const annotation = useAnnotation();
  const ready = useAnnotationState((state) => state.status === 'ready');
  const cover = usePageList()[0]?.ref;
  const [ref, setRef] = useState<AnnotationRef | null>(null);
  const started = useRef(false);
  useAnnotationList(); // re-render as the annotations change, for the reads below

  const add = async () => {
    if (!cover) return;
    const { annotation: square } = await annotation.create(cover, {
      subtype: 'square',
      box: { x: 72, y: 592, width: 200, height: 100 },
      color: '#0078ff',
      strokeWidth: 3,
    });
    setRef(square.ref);
  };

  // On load: one rectangle, to change.
  useEffect(() => {
    if (!ready || started.current) return;
    started.current = true;
    void add();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [ready]);

  const square = ref ? annotation.get(ref) : null;
  const canUpdate = square !== null && annotation.canUpdate(square.ref);
  const canDelete = square !== null && annotation.canDelete(square.ref);

  let status = 'No rectangle';
  if (square) status = annotation.isPending(square.ref) ? 'Saving…' : 'Saved';

  return (
    <div className="toolbar">
      <button type="button" className="button" disabled={square !== null} onClick={add}>
        Create
      </button>
      <button
        type="button"
        className="button"
        disabled={!canUpdate}
        onClick={() => square && annotation.update(square.ref, { color: '#dc143c' })}
      >
        Make it red
      </button>
      <button
        type="button"
        className="button"
        disabled={!canUpdate}
        onClick={() => {
          if (!square) return;
          const { rect } = square;
          void annotation.update(square.ref, { rect: { ...rect, x: rect.x + 40 } }); // 40 points right
        }}
      >
        Move right
      </button>
      <button
        type="button"
        className="button"
        disabled={!canUpdate}
        onClick={() => square && annotation.update(square.ref, { rotation: 90 })}
      >
        Turn to 90°
      </button>
      <button
        type="button"
        className="button"
        disabled={!canDelete}
        onClick={() => {
          if (!square) return;
          void annotation.delete(square.ref);
          setRef(null);
        }}
      >
        Delete
      </button>
      <span className="spacer" />
      <output className="readout">{status}</output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <SquareControls />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}

Try it: each button is one call. Saving… shows while the engine writes a change.

An annotation here is the engine’s record: the same kinds, fields and names as the engine’s annotation pages. Each kind’s page there lists its fields. These pages cover what the plugin adds on top.

Read annotations#

annotation.list(); // every annotation, in drawing order
annotation.list({ pages: [page.ref] }); // one page
annotation.list({ subtype: 'highlight' }); // one kind
annotation.get(ref); // one, or null
annotation.getAt(page.ref, { x: 120, y: 80 }); // the topmost one at a point, or null

To keep a component up to date, use useAnnotationList(). Your component re-renders when the list changes:

list.tsx
import { useAnnotationList } from '@embedpdf/react/annotation';

export function HighlightCount() {
  const highlights = useAnnotationList({ subtype: 'highlight' });

  return <p>{highlights.length} highlights</p>;
}

Loading live preview…

import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import {
  AnnotationLayer,
  annotationKey,
  annotationPlugin,
  useAnnotation,
  useAnnotationList,
  useAnnotationState,
  type AnnotationSubtype,
} from '@embedpdf/react/annotation';
import { localEngine } from '@embedpdf/engine';

import './list.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), annotationPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// A line of text, as the four corners a highlight takes.
const quad = (x: number, y: number, width: number, height: number) => ({
  upperLeft: { x, y },
  upperRight: { x: x + width, y },
  lowerLeft: { x, y: y + height },
  lowerRight: { x: x + width, y: y + height },
});

// On load: highlights, notes and a rectangle on the first two pages.
function AddAnnotations() {
  const annotation = useAnnotation();
  const ready = useAnnotationState((state) => state.status === 'ready');
  const [cover, second] = usePageList();
  const added = useRef(false);

  useEffect(() => {
    if (!ready || !cover || !second || added.current) return;
    added.current = true;
    void annotation.create(cover.ref, {
      subtype: 'highlight',
      quadPoints: [quad(106, 218, 352, 49)],
      color: '#ffcd45',
      contents: 'The title',
    });
    void annotation.create(cover.ref, {
      subtype: 'text',
      rect: { x: 470, y: 232, width: 20, height: 20 },
      contents: 'Can we shorten it?',
      color: '#facc15',
    });
    void annotation.create(second.ref, {
      subtype: 'highlight',
      quadPoints: [quad(57, 57, 242, 36), quad(57, 93, 265, 36)],
      color: '#ffcd45',
      contents: 'The opening line',
    });
    void annotation.create(second.ref, {
      subtype: 'square',
      box: { x: 52, y: 580, width: 470, height: 64 },
      color: '#e5484d',
      strokeWidth: 2,
      contents: 'Rewrite this paragraph',
    });
  }, [annotation, ready, cover, second]);

  return null;
}

const KINDS: { label: string; subtype?: AnnotationSubtype }[] = [
  { label: 'All' },
  { label: 'Highlights', subtype: 'highlight' },
  { label: 'Notes', subtype: 'text' },
  { label: 'Rectangles', subtype: 'square' },
];

// The annotations of one kind, in drawing order. A click shows one on its page.
function AnnotationList() {
  const [kind, setKind] = useState(KINDS[0]!);
  const annotations = useAnnotationList(kind.subtype ? { subtype: kind.subtype } : undefined);
  const pages = usePageList();
  const stage = useStage();
  const annotation = useAnnotation();

  const pageNumberOf = (objectNumber: number) =>
    pages.findIndex((page) => page.ref.objectNumber === objectNumber) + 1;

  return (
    <div className="panel">
      <div className="segmented" role="group" aria-label="Kind">
        {KINDS.map((each) => (
          <button
            key={each.label}
            type="button"
            aria-pressed={each === kind}
            onClick={() => setKind(each)}
          >
            {each.label}
          </button>
        ))}
      </div>
      <p className="count">{annotations.length} found</p>
      <ul className="items">
        {annotations.map((each) => (
          <li key={annotationKey(each.ref)}>
            <button
              type="button"
              className="item"
              onClick={() => {
                stage.reveal(each.page, { rect: each.rect });
                annotation.selection.set([each.ref]);
              }}
            >
              <span className="kind">
                {each.subtype} · page {pageNumberOf(each.page.objectNumber)}
              </span>
              <span>{each.contents ?? '—'}</span>
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <AddAnnotations />
        <div className="viewer">
          <Stage className="stage">
            {() => (
              <>
                <RenderLayer />
                <AnnotationLayer />
              </>
            )}
          </Stage>
          <AnnotationList />
        </div>
      </DocumentGate>
    </Viewer>
  );
}

Try it: pick a kind, then click a row to go to it.

Each annotation has a subtype (its kind), a rect (where it is on its page), a page and a ref. Keep the ref: it’s how you change or delete the annotation later. Positions are in points from the top-left of the page; see Coordinates.

Annotations are loaded in the background when a document opens. useAnnotationState() has a status: 'loading' until they’re all in, then 'ready'.

Create an annotation#

create() takes the page, then the annotation’s kind and fields:

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

Fields you leave out get the kind’s default. For example, a new annotation prints with the page unless you pass print: false. Every kind the engine can create, the plugin can too; the engine’s pages show each one.

Some options change how the plugin treats the new annotation:

await annotation.create(page.ref, { subtype: 'square', box }, undefined, {
  tool: 'square',
  select: true,
});
OptionWhat it does
toolStarts from that tool’s defaults, the user’s current drawing style, before your own fields.
selectSelects the new annotation, as drawing it would. Without it, code never changes the selection.

Stamps and file attachments carry bytes. They go in the third argument, resources, named for what they are: a stamp’s appearance, an attachment’s file.

const image = await (await fetch('/approved.png')).blob();
await annotation.create(page.ref, { subtype: 'stamp', box }, { appearance: image });
 
// A file pinned to the page, drawn as an icon, 20 × 20 like a note
await annotation.create(
  page.ref,
  { subtype: 'file-attachment', rect: { x: 40, y: 60, width: 20, height: 20 } },
  { file: pickedFile }, // a File brings its name and type
);

For stamps from a library, see Stamps.

From the selected text#

createFromSelection() turns the text a person selected into a highlight, an underline, a strikeout or a squiggly line, one per page:

const { annotations } = await annotation.createFromSelection('highlight');

It takes a tool id, so your own markup tools work too. The text selection is cleared afterwards; pass { clear: false } to keep it.

Loading live preview…

import { useEffect, useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import {
  SelectionLayer,
  selectionPlugin,
  useSelection,
  useSelectionState,
} from '@embedpdf/react/selection';
import { AnnotationLayer, annotationPlugin, useAnnotation } from '@embedpdf/react/annotation';
import { localEngine } from '@embedpdf/engine';

import './from-selection.css';

const engine = localEngine();
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  selectionPlugin(),
  annotationPlugin(),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// On the cover: the characters of the title's first line.
const FIRST_LINE = { start: 10, count: 17 };

const MARKUP = [
  { tool: 'highlight', label: 'Highlight' },
  { tool: 'underline', label: 'Underline' },
  { tool: 'strikeout', label: 'Strike out' },
  { tool: 'squiggly', label: 'Squiggly' },
];

// The selected text becomes a mark, one per page; the selection is cleared afterwards.
function MarkupToolbar() {
  const annotation = useAnnotation();
  const selection = useSelection();
  const { hasSelection } = useSelectionState();
  const cover = usePageList()[0]?.ref;
  const [status, setStatus] = useState('');

  // On load: the title's first line is selected, ready to mark.
  useEffect(() => {
    if (cover) selection.select({ page: cover, ...FIRST_LINE });
  }, [selection, cover]);

  const mark = async (tool: string, label: string) => {
    const { annotations } = await annotation.createFromSelection(tool);
    setStatus(`${label}: ${annotations.length} made`);
  };

  return (
    <div className="toolbar">
      {MARKUP.map(({ tool, label }) => (
        <button
          key={tool}
          type="button"
          className="button"
          disabled={!hasSelection}
          onClick={() => void mark(tool, label)}
        >
          {label}
        </button>
      ))}
      <span className="spacer" />
      <output className="readout">
        {hasSelection ? 'Text selected' : status || 'Select text'}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <MarkupToolbar />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <SelectionLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}

Try it: the title’s first line is selected: press Highlight. Then select other words with the pointer and mark them.

Change an annotation#

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

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

A field you leave out keeps its value, and null removes it ({ interiorColor: null } for no fill). You can also pass an annotation you read straight back, which makes “change a copy, then save it” simple:

const square = annotation.get(ref);
if (square?.subtype === 'square') await annotation.update(ref, { ...square, opacity: 0.5 });

To move or resize any annotation, send a new rect:

const { rect } = annotation.get(ref)!;
await annotation.update(ref, { rect: { ...rect, x: rect.x + 40 } }); // 40 points right

For a shape drawn from points, like a line or ink, the engine moves every point for you. The rules for resizing are on the engine’s Annotations page.

Rotation is a field too: update(ref, { rotation: 90 }) turns a shape or a stamp to 90° clockwise.

Delete an annotation#

await annotation.delete(ref);

Deleting an annotation also deletes its popup and its replies, as other PDF apps do.

Change the drawing order#

Annotations on a page are drawn in order, and the last one is on top. reorder() moves annotations together, in the order you pass them, next to another annotation of the page or to the back or the front:

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

Form fields are drawn above every annotation and keep their own order, so reorder() never places an annotation above a field.

Loading live preview…

import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import {
  AnnotationLayer,
  annotationKey,
  annotationPlugin,
  useAnnotation,
  useAnnotationList,
  useAnnotationState,
} from '@embedpdf/react/annotation';
import { localEngine } from '@embedpdf/engine';

import './order.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), annotationPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const COLORS = [
  { color: '#e5484d', fill: '#ffd1d3' },
  { color: '#30a46c', fill: '#c9f0da' },
  { color: '#1e90ff', fill: '#cfe6ff' },
];

// On load: three filled rectangles stacked on the cover, the middle one selected.
function AddRectangles() {
  const annotation = useAnnotation();
  const ready = useAnnotationState((state) => state.status === 'ready');
  const cover = usePageList()[0]?.ref;
  const added = useRef(false);

  useEffect(() => {
    if (!ready || !cover || added.current) return;
    added.current = true;
    COLORS.forEach(({ color, fill }, index) => {
      void annotation.create(
        cover,
        {
          subtype: 'square',
          box: { x: 300 + index * 50, y: 520 + index * 30, width: 160, height: 90 },
          color,
          interiorColor: fill,
          strokeWidth: 3,
        },
        undefined,
        { select: index === 1 },
      );
    });
  }, [annotation, ready, cover]);

  return null;
}

// The last annotation on a page is drawn on top.
function OrderControls() {
  const annotation = useAnnotation();
  const { selected } = useAnnotationState();
  const [first] = selected;
  const onPage = useAnnotationList(first ? { pages: [first.page] } : undefined);
  const position = first
    ? onPage.findIndex((a) => annotationKey(a.ref) === annotationKey(first.ref))
    : -1;

  return (
    <div className="toolbar">
      <button
        type="button"
        className="button"
        disabled={!first || selected.length !== 1}
        onClick={() => first && annotation.reorder([first.ref], 'start')}
      >
        Send to back
      </button>
      <button
        type="button"
        className="button"
        disabled={!first || selected.length !== 1}
        onClick={() => first && annotation.reorder([first.ref], 'end')}
      >
        Bring to front
      </button>
      <span className="spacer" />
      <output className="readout">
        {position >= 0
          ? `${position + 1} of ${onPage.length}, from the back`
          : 'Select a rectangle'}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <AddRectangles />
        <OrderControls />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}

Try it: send the green rectangle to the back, then select another and bring it to the front.

Changes show at once#

A change shows the moment you make it. The engine saves it in the background, and your await resolves once it has. Until then the annotation is pending:

annotation.isPending(ref); // true while the engine is still saving a change to it

You rarely need this: get() and list() already show the change. It’s useful for a small “saving…” hint.

If the engine refuses a change (a lock set by someone else, or permission taken away), the change disappears again, and an event says why:

write-failed.tsx
import { useAnnotationEvent } from '@embedpdf/react/annotation';

import { toast } from './toast';

export function SaveErrors() {
  useAnnotationEvent(
    (annotation) => annotation.onWriteFailed,
    ({ error }) => toast(`Couldn't save: ${error.message}`),
  );

  return null;
}

The await of the call that made the change rejects with the same error.

Copy an annotation#

A read passed to create() makes the same annotation again, on another page or in another document:

const copy = annotation.get(ref)!;
 
await annotation.create(otherPage.ref, copy);

Loading live preview…

import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import {
  AnnotationLayer,
  annotationPlugin,
  useAnnotation,
  useAnnotationState,
} from '@embedpdf/react/annotation';
import { localEngine } from '@embedpdf/engine';

import './copy.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), annotationPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// On load: a rectangle and a text box on the cover, the rectangle selected and in view.
function AddAnnotations() {
  const annotation = useAnnotation();
  const stage = useStage();
  const ready = useAnnotationState((state) => state.status === 'ready');
  const cover = usePageList()[0]?.ref;
  const added = useRef(false);

  useEffect(() => {
    if (!ready || !cover || added.current) return;
    added.current = true;
    void annotation.create(cover, {
      subtype: 'free-text',
      box: { x: 300, y: 512, width: 230, height: 40 },
      contents: 'Copy me too',
      fontSize: 16,
      fontColor: '#1a2748',
      interiorColor: '#fffbe6',
    });
    void annotation
      .create(
        cover,
        {
          subtype: 'square',
          box: { x: 96, y: 506, width: 178, height: 54 },
          color: '#e5484d',
          interiorColor: '#ffe4e1',
          strokeWidth: 3,
        },
        undefined,
        { select: true },
      )
      .then(({ annotation: made }) => stage.reveal(cover, { rect: made.rect }));
  }, [annotation, stage, ready, cover]);

  return null;
}

// A read passed to create() makes the same annotation again, here on the next page.
function CopyButton() {
  const annotation = useAnnotation();
  const stage = useStage();
  const pages = usePageList();
  const { selected } = useAnnotationState();
  const [status, setStatus] = useState('');
  const [first] = selected;
  const index = first
    ? pages.findIndex((page) => page.ref.objectNumber === first.page.objectNumber)
    : -1;
  const next = pages[index + 1];

  const copyToNextPage = async () => {
    if (!first || !next) return;
    const copy = annotation.get(first.ref)!;
    const { annotation: made } = await annotation.create(next.ref, copy, undefined, {
      select: true, // so the next click copies the copy, a page further
    });
    stage.reveal(next.ref, { rect: made.rect });
    setStatus(`Copied to page ${next.index + 1}`);
  };

  return (
    <div className="toolbar">
      <button
        type="button"
        className="button"
        disabled={selected.length !== 1 || !next}
        onClick={() => void copyToNextPage()}
      >
        Copy to the next page
      </button>
      <span className="spacer" />
      <output className="readout">
        {status || (first ? 'Ready to copy' : 'Select an annotation to copy')}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <AddAnnotations />
        <CopyButton />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}

Try it: copy the rectangle, then copy the copy: it moves a page further each time. Select the text box to copy it instead.

Stamps and file attachments also need their bytes. Download them first:

const drawing = await annotation.downloadResource(stamp.ref, 'appearance');
await annotation.create(otherPage.ref, stamp, { appearance: drawing });
 
const file = await annotation.downloadResource(attachment.ref, 'file');
await annotation.create(otherPage.ref, attachment, { file });

To copy on the same page, clear its unique name first: { ...copy, nm: null }. A name is unique per page, so a copy with a taken name is refused.

To copy many annotations at once, use export and import.

Methods#

What useAnnotation() gives you directly. Its parts have their own pages: selection, draft and text are on Selecting & editing, tools and stamps on Tools & defaults, and comments on Comments & replies.

MethodWhat it does
get(ref)One annotation as it shows now, pending changes included, or null
list({ pages?, subtype? })Every annotation in drawing order, or the ones on some pages or of one kind. In a component, use useAnnotationList(filter). Read annotations
getAt(page, point)The topmost annotation at a point on a page, or null. A selected annotation’s handles count as the annotation.
isPending(ref)Whether the engine is still saving a change to it. Changes show at once
create(page, fields, resources?, { tool?, select? })Creates an annotation from its kind and fields, and shows it at once. A stamp’s drawing or an attachment’s file goes in resources, as { appearance } or { file }. Resolves { annotation }. Create an annotation
createFromSelection(tool, { clear? })Turns the selected text into a highlight, an underline, a strikeout or a squiggly line, one per page. Resolves { annotations }. From the selected text
update(ref, changes, resources?)Changes only the fields you pass, and null removes one. New resources replace a stamp’s drawing or an attachment’s file. Resolves { annotation }. Change an annotation
delete(ref)Deletes an annotation, with its popup and its replies. Delete an annotation
reorder(refs, position)Moves annotations to a new place in their page’s drawing order: next to another annotation, or to the back or front. Change the drawing order
export({ refs?, pages? }), import(bundle, options?)Takes annotations out as a bundle, with the bytes they carry, or puts a bundle in. import resolves { annotations, refMap, dropped }. Export and import
downloadResource(ref, role)Downloads a stamp’s drawing ('appearance') or an attachment’s file ('file'), ready to pass back to create(). Copy an annotation
links.get(ref), links.set(ref, target), links.clear(ref)Where an annotation links to, such as a page or a website: read it, set it, or remove the link
refresh()Reads every annotation from the engine again. Changes arrive by themselves, so you rarely need it.
getSettings(), updateSettings(changes), resetSettings()Reads snap, chrome and afterCreate, changes them while the app runs, or goes back to what you registered. In a component, use useAnnotationSettings(). Settings
canRead(), canCreate(), canUpdate(ref), canDelete(ref)Whether the user may see, create, change or delete annotations. The checks that take a ref answer for that annotation. Permissions

Every method that returns a promise also takes a signal in its options, to cancel it.

Permissions#

PermissionWhat it allowsCheck
doc.annotate.readSeeing annotationsannotation.canRead()
annotations:createCreating themannotation.canCreate()
annotations:updateChanging themannotation.canUpdate(ref)
annotations:deleteDeleting themannotation.canDelete(ref)

A permission can end in :self, the user’s own annotations, or :group, their group’s. annotations:update:self lets someone change what they made and only look at the rest, so the checks that take a ref answer per annotation. They also follow the annotation’s own flags: for a locked one, canDelete(ref) is false, so your UI offers no delete button, even though your own code may still delete it (see Locked annotations). More on Permissions.

Good to know#

Locked annotations#

The locked flag stops people from moving, resizing or deleting an annotation, and lockedContents stops them from changing its text. These flags are for the people using your app: your own code can still change a locked annotation, as the engine can. To stop the code too, check the flags first.

What a copy can’t carry#

A few annotations can’t be made again from their read, and create() refuses them with invalid-input:

  • a link whose action the engine can’t create, such as one that runs JavaScript;
  • a popup whose annotation is gone;
  • a value another app wrote that the engine can’t write, such as a beveled border;
  • a kind the engine doesn’t know (subtype: 'unsupported').

Import leaves these out and tells you which ones, and why.

Changes other people make#

On a shared document, other people’s changes arrive as events and show up in list() by themselves. See Events, saving & sharing.

Was this page helpful?

Your feedback goes directly to the documentation team.