EmbedPDF

Events, saving & sharing

Every annotation change fires an event, whoever made it: the user, your code, or someone else on a shared document. Listen for them to keep your own UI or server in step:

events.vue
<script setup lang="ts">
import { useAnnotationEvent } from '@embedpdf/vue/annotation';

useAnnotationEvent(
  (annotation) => annotation.onCreated,
  ({ annotation, origin }) => {
    console.log(`${annotation.author} added a ${annotation.subtype}`);
  },
);
</script>

<template>
  <slot />
</template>

Loading live preview…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import ActivityLog from './ActivityLog.vue';
import AddRectangle from './AddRectangle.vue';
import Toolbar from './Toolbar.vue';

import '../events.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()) };
};
</script>

<template>
  <Viewer
    :engine="engine"
    :plugins="plugins"
    :identity="{ userId: 'u_381', displayName: 'Dana Smith' }"
    :initial-documents="[{ source: ebook }]"
  >
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <AddRectangle />
      <Toolbar />
      <div class="viewer">
        <Stage class="stage">
          <template #page>
            <RenderLayer />
            <AnnotationLayer />
          </template>
        </Stage>
        <ActivityLog />
      </div>
    </DocumentGate>
  </Viewer>
</template>

Try it: draw, move and delete. Each change is logged once the engine has saved it, as local: this viewer made it.

The composable listens while the component is mounted. Outside a component, call the event on the plugin’s API; it returns a function that stops listening:

const stop = annotation.onCreated(({ annotation }) => {
  /* … */
});

Events#

EventFires whenCarries
onCreatedAn annotation was added.annotation, origin
onUpdatedAn annotation was changed.annotation, origin
onDeletedAnnotations were deleted: one, plus its popup and replies.refs, page, origin
onReorderedThe drawing order on a page changed.page, order, origin
onResyncedAnnotations were read again, as after loading a document.pages
onWriteFailedThe engine refused a change the user made.refs, error
onSettingsChangedA setting changed, through updateSettings() or resetSettings().settings, changed
  • Once, and after it’s saved. onCreated, onUpdated, onDeleted and onReordered fire when the engine has saved the change, never while it’s still pending. A change fires once, however many screens show it.
  • Loading isn’t a list of creates. When a document opens, its annotations arrive as one onResynced, not as an onCreated per annotation. The same happens after a change that touches whole pages, such as flattening or redaction. Then read the pages it names again with list().
  • annotation is the record after the change. It’s what get() returns now.
  • order is the whole page. After a reorder, order holds the page’s annotations as refs, bottom to top, the same order list({ pages: [page] }) returns.

The selection, a shape being drawn, text editing and hover have events of their own (onSelectionChanged, onDraftChanged, onEditingChanged, onHoverChanged); see Selecting & editing. A tool’s defaults have tools.onDefaultsChanged; see Tools & defaults.

Where a change came from#

origin says who made the change:

FieldWhat it is
origin.kind'local' when this viewer made the change, 'remote' when someone else did.
origin.subThe user who made it (on a shared document).
origin.tsWhen it happened, in milliseconds.
origin.txShared by every change one call made, such as an import.
remote-toast.vue
<script setup lang="ts">
import { useAnnotationEvent } from '@embedpdf/vue/annotation';
import { toast } from './toast'; // your own

useAnnotationEvent(
  (annotation) => annotation.onUpdated,
  ({ annotation, origin }) => {
    if (origin.kind === 'remote') toast(`${annotation.author} changed a comment`);
  },
);
</script>

<template>
  <slot />
</template>

These are the engine’s own events, so the fields mean the same as on Events.

Save the PDF#

Annotations are part of the document. Save the document and they’re in the file:

const bytes = await documents.download(); // the whole PDF, with every annotation

Loading live preview…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import AddRectangle from './AddRectangle.vue';
import SaveBar from './SaveBar.vue';

import '../download.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()) };
};
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <AddRectangle />
      <SaveBar />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>

A save waits for pending changes, so what you save is what the user sees. For the difference between a quick save that appends the changes and a full rewrite, see Saving.

Export and import#

To take annotations out of a document, as data rather than inside a PDF, export them. One call gives you a bundle: every annotation with the bytes it carries (a stamp’s drawing, an attached file). One call puts a bundle into a document. AnnotationTransfer comes from @embedpdf/vue/annotation:

const bundle = await annotation.export(); // everything
const text = AnnotationTransfer.stringify(bundle); // one JSON text, ready to store
 
// later, in the same document or another one
const { annotations, dropped } = await annotation.import(await AnnotationTransfer.parse(text), {
  pages: 'by-position',
});

Use it to:

  • keep annotations in your own database, apart from the PDF;
  • carry a review over to a new version of a document;
  • let a user paste annotations from one document into another.

Loading live preview…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import Transfer from './Transfer.vue';

import '../export-import.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()) };
};
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <div class="viewer">
        <Stage class="stage">
          <template #page>
            <RenderLayer />
            <AnnotationLayer />
          </template>
        </Stage>
        <Transfer />
      </div>
    </DocumentGate>
  </Viewer>
</template>

Try it: Delete all, then Import: they come back from the text. Import again: the annotations with a name are left out, since they’re there already.

Export waits for pending changes, and imported annotations show at once, like any other change.

What to export:

await annotation.export({ refs: annotation.selection.list().map((a) => a.ref) }); // the selection
await annotation.export({ pages: [page.ref] }); // one page

A selection brings what it needs: a reply brings the comment it answers, and a comment brings its replies.

How to import:

OptionWhat it doesDefault
pagesWhere each page’s annotations go: 'same' (the same page), 'by-position', or a list of pairs.'same'
attribution'restore' keeps who wrote each annotation, 'stamp' makes the importing user the author.'restore'

Use 'stamp' when a user pastes annotations: they become theirs. 'restore' writes annotations in other people’s names, so it needs a special permission; keep it for trusted jobs, such as loading from your own database.

The result tells you what happened:

  • annotations: the new annotations, in bundle order.
  • refMap: each annotation’s ref in the bundle, and its new ref here.
  • dropped: what was left out, and why. For example, an annotation whose name is already taken on its page. Importing the same bundle twice doesn’t duplicate annotations with a name.

The engine’s Import & export page has every option, reason and limit.

Keep annotations in your own database#

There are two ways to store annotations outside the PDF.

Store a bundle per document. Export when the user saves, store the text, and import it when the document opens again. It’s one row per document and one call each way:

// on save
await api.put(`/reviews/${docId}`, AnnotationTransfer.stringify(await annotation.export()));
 
// on open, once the annotations are loaded (status 'ready')
const text = await api.get(`/reviews/${docId}`);
if (text) await annotation.import(await AnnotationTransfer.parse(text));

Store each annotation as it changes. Listen for the events and write each change to your server. Key your rows by the annotation’s ref:

sync.vue
<script setup lang="ts">
import { annotationKey, useAnnotationEvent } from '@embedpdf/vue/annotation';
import { api } from './api'; // your own

useAnnotationEvent(
  (annotation) => annotation.onCreated,
  ({ annotation, origin }) => {
    if (origin.kind === 'local')
      api.put(`/annotations/${annotationKey(annotation.ref)}`, annotation);
  },
);
</script>

<template>
  <slot />
</template>

Handle onUpdated the same way, and onDeleted by deleting each of its refs. On onResynced, compare the pages it names with list(). Only send local changes: a remote one was already saved by whoever made it.

Which id to store#

Store the annotation’s ref. It’s plain data, and it stays valid after the document is saved and opened again. To use it as a key in a map or a URL, turn it into a string with annotationKey(ref). See Refs & identity.

Good to know#

Unsaved changes#

To warn before someone leaves with annotations that aren’t downloaded yet, use hasUnsavedChanges from useDocument(); see Warn before changes are lost.

Events on a shared document#

The local engine’s events come from this tab: the changes made here, by the user or your code.

Was this page helpful?

Your feedback goes directly to the documentation team.