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:
<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#
| Event | Fires when | Carries |
|---|---|---|
onCreated | An annotation was added. | annotation, origin |
onUpdated | An annotation was changed. | annotation, origin |
onDeleted | Annotations were deleted: one, plus its popup and replies. | refs, page, origin |
onReordered | The drawing order on a page changed. | page, order, origin |
onResynced | Annotations were read again, as after loading a document. | pages |
onWriteFailed | The engine refused a change the user made. | refs, error |
onSettingsChanged | A setting changed, through updateSettings() or resetSettings(). | settings, changed |
- Once, and after it’s saved.
onCreated,onUpdated,onDeletedandonReorderedfire 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 anonCreatedper annotation. The same happens after a change that touches whole pages, such as flattening or redaction. Then read thepagesit names again withlist(). annotationis the record after the change. It’s whatget()returns now.orderis the whole page. After a reorder,orderholds the page’s annotations as refs, bottom to top, the same orderlist({ 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:
| Field | What it is |
|---|---|
origin.kind | 'local' when this viewer made the change, 'remote' when someone else did. |
origin.sub | The user who made it (on a shared document). |
origin.ts | When it happened, in milliseconds. |
origin.tx | Shared by every change one call made, such as an import. |
<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 annotationLoading 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 pageA selection brings what it needs: a reply brings the comment it answers, and a comment brings its replies.
How to import:
| Option | What it does | Default |
|---|---|---|
pages | Where 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:
<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.
Your feedback goes directly to the documentation team.