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.svelte
<script lang="ts">
  import { useAnnotationEvent } from '@embedpdf/svelte/annotation';

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

Loading live preview…

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

  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>

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

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

The function 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.svelte
<script lang="ts">
  import { useAnnotationEvent } from '@embedpdf/svelte/annotation';
  import { toast } from './toast'; // your own

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

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 lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { localEngine } from '@embedpdf/engine';
  import AddRectangle from './AddRectangle.svelte';
  import SaveBar from './SaveBar.svelte';

  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>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <AddRectangle />
    <SaveBar />
    <Stage class="stage">
      <RenderLayer />
      <AnnotationLayer />
    </Stage>
  </DocumentGate>
</Viewer>

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/svelte/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 lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
  import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
  import { interactionPlugin } from '@embedpdf/svelte/interaction';
  import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
  import { localEngine } from '@embedpdf/engine';
  import Transfer from './Transfer.svelte';

  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>

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <div class="viewer">
      <Stage class="stage">
        <RenderLayer />
        <AnnotationLayer />
      </Stage>
      <Transfer />
    </div>
  </DocumentGate>
</Viewer>

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.svelte
<script lang="ts">
  import { annotationKey, useAnnotationEvent } from '@embedpdf/svelte/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>

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.