Skip to content

Commit 894b07d

Browse files
authored
Merge pull request #571 from embedpdf/fix/add-missing-types-and-functions-annotation
Fix/add missing types and functions annotation
2 parents 4db4e77 + e3d1834 commit 894b07d

21 files changed

Lines changed: 1760 additions & 12 deletions

File tree

.changeset/get-annotations-api.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@embedpdf/plugin-annotation': patch
3+
---
4+
5+
Add `getAnnotations(options?)` method to retrieve all tracked annotations, optionally filtered by page index. Available on both `AnnotationCapability` and `AnnotationScope`.

.changeset/snippet-export-types.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@embedpdf/snippet': patch
3+
---
4+
5+
Export missing annotation types from the snippet package: `AnnotationTransferItem`, `ExportAnnotationsOptions`, `GetAnnotationsOptions`, `TrackedAnnotation`, `AnnotationTool`, `PdfAnnotationSubtype`, and `PdfStampAnnoObject`.

examples/svelte-tailwind/src/examples/headless/annotation-import-export-example-content.svelte

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
let { documentId }: Props = $props();
2727
2828
let activeTool = $state<string | null>(null);
29-
let exported = $state<AnnotationTransferItem[] | null>(null);
29+
let exported: AnnotationTransferItem[] | null = null;
30+
let hasExported = $state(false);
3031
let status = $state<string | null>(null);
3132
let annotationCount = $state(0);
3233
@@ -71,6 +72,7 @@
7172
annotationApi.exportAnnotations().wait(
7273
(result) => {
7374
exported = result;
75+
hasExported = true;
7476
status = `Exported ${result.length} annotation${result.length !== 1 ? 's' : ''}`;
7577
},
7678
() => {
@@ -151,11 +153,11 @@
151153
<button
152154
type="button"
153155
onclick={handleImport}
154-
disabled={!exported}
156+
disabled={!hasExported}
155157
class="inline-flex items-center gap-1.5 rounded-md bg-blue-500 px-2.5 py-1.5 text-xs font-medium text-white shadow-sm transition-all hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-50"
156158
>
157159
<Upload size={14} />
158-
Import{exported ? ` (${exported.length})` : ''}
160+
Import{hasExported && exported ? ` (${exported.length})` : ''}
159161
</button>
160162
</div>
161163

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
<script lang="ts">
2+
import { onDestroy } from 'svelte';
3+
import {
4+
PDFViewer,
5+
type EmbedPdfContainer,
6+
type PluginRegistry,
7+
type AnnotationPlugin,
8+
type AnnotationCapability,
9+
type AnnotationTool,
10+
type AnnotationTransferItem,
11+
PdfAnnotationSubtype,
12+
type PdfStampAnnoObject,
13+
} from '@embedpdf/svelte-pdf-viewer';
14+
import {
15+
Check,
16+
X,
17+
Pencil,
18+
Square,
19+
Highlighter,
20+
Type,
21+
Download,
22+
Upload,
23+
Trash2,
24+
MousePointer2,
25+
} from 'lucide-svelte';
26+
27+
interface Props {
28+
themePreference?: 'light' | 'dark';
29+
}
30+
31+
let { themePreference = 'light' }: Props = $props();
32+
33+
let container = $state<EmbedPdfContainer | null>(null);
34+
let annotationApi = $state<AnnotationCapability | null>(null);
35+
let activeTool = $state<string | null>(null);
36+
let annotationCount = $state(0);
37+
let exported: AnnotationTransferItem[] | null = null;
38+
let hasExported = $state(false);
39+
let status = $state<string | null>(null);
40+
let cleanups: (() => void)[] = [];
41+
42+
const handleInit = (c: EmbedPdfContainer) => {
43+
container = c;
44+
};
45+
46+
const handleReady = (registry: PluginRegistry) => {
47+
const api = registry.getPlugin<AnnotationPlugin>('annotation')?.provides();
48+
if (!api) return;
49+
50+
annotationApi = api;
51+
52+
api.addTool<AnnotationTool<PdfStampAnnoObject>>({
53+
id: 'stampCheckmark',
54+
name: 'Checkmark',
55+
interaction: { exclusive: true, cursor: 'crosshair' },
56+
matchScore: () => 0,
57+
defaults: {
58+
type: PdfAnnotationSubtype.STAMP,
59+
imageSrc: '/circle-checkmark.png',
60+
imageSize: { width: 30, height: 30 },
61+
},
62+
behavior: {
63+
showGhost: true,
64+
deactivateToolAfterCreate: true,
65+
selectAfterCreate: true,
66+
},
67+
});
68+
69+
api.addTool<AnnotationTool<PdfStampAnnoObject>>({
70+
id: 'stampCross',
71+
name: 'Cross',
72+
interaction: { exclusive: true, cursor: 'crosshair' },
73+
matchScore: () => 0,
74+
defaults: {
75+
type: PdfAnnotationSubtype.STAMP,
76+
imageSrc: '/circle-cross.png',
77+
imageSize: { width: 30, height: 30 },
78+
},
79+
behavior: {
80+
showGhost: true,
81+
deactivateToolAfterCreate: true,
82+
selectAfterCreate: true,
83+
},
84+
});
85+
86+
const cleanupTool = api.onActiveToolChange(({ tool }) => {
87+
activeTool = tool?.id || null;
88+
});
89+
cleanups.push(cleanupTool);
90+
91+
const cleanupEvents = api.onAnnotationEvent((event) => {
92+
if (event.type === 'create' || event.type === 'delete' || event.type === 'loaded') {
93+
annotationCount = api.getAnnotations().length;
94+
}
95+
});
96+
cleanups.push(cleanupEvents);
97+
98+
annotationCount = api.getAnnotations().length;
99+
};
100+
101+
$effect(() => {
102+
container?.setTheme({ preference: themePreference });
103+
});
104+
105+
onDestroy(() => {
106+
cleanups.forEach((cleanup) => cleanup());
107+
});
108+
109+
const setTool = (toolId: string | null) => {
110+
annotationApi?.setActiveTool(toolId);
111+
};
112+
113+
const handleExport = () => {
114+
if (!annotationApi) return;
115+
annotationApi.exportAnnotations().wait(
116+
(result) => {
117+
exported = result;
118+
hasExported = true;
119+
status = `Exported ${result.length} annotation${result.length !== 1 ? 's' : ''}`;
120+
},
121+
() => {
122+
status = 'Export failed';
123+
},
124+
);
125+
};
126+
127+
const handleClear = () => {
128+
if (!annotationApi) return;
129+
annotationApi.deleteAllAnnotations();
130+
status = 'Cleared all annotations';
131+
};
132+
133+
const handleImport = () => {
134+
if (!annotationApi || !exported) return;
135+
annotationApi.importAnnotations(exported);
136+
status = `Imported ${exported.length} annotation${exported.length !== 1 ? 's' : ''}`;
137+
};
138+
139+
const tools = [
140+
{ id: null, name: 'Select', icon: MousePointer2 },
141+
{ id: 'stampCheckmark', name: 'Checkmark', icon: Check },
142+
{ id: 'stampCross', name: 'Cross', icon: X },
143+
{ id: 'ink', name: 'Pen', icon: Pencil },
144+
{ id: 'square', name: 'Square', icon: Square },
145+
{ id: 'highlight', name: 'Highlight', icon: Highlighter },
146+
{ id: 'freeText', name: 'Text', icon: Type },
147+
];
148+
</script>
149+
150+
<div class="flex flex-col gap-4">
151+
<!-- Toolbar -->
152+
<div
153+
class="flex flex-col gap-3 rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800"
154+
>
155+
<!-- Annotation tools -->
156+
<div class="flex flex-wrap items-center gap-3">
157+
<span class="text-xs font-medium uppercase tracking-wide text-gray-600 dark:text-gray-300">
158+
Tools
159+
</span>
160+
<div class="h-4 w-px bg-gray-300 dark:bg-gray-600"></div>
161+
<div class="flex items-center gap-1">
162+
{#each tools as tool (tool.id ?? 'select')}
163+
<button
164+
type="button"
165+
onclick={() => setTool(activeTool === tool.id && tool.id !== null ? null : tool.id)}
166+
class="rounded p-2 transition-colors {activeTool === tool.id
167+
? 'bg-white text-blue-600 shadow-sm dark:bg-gray-700 dark:text-blue-400'
168+
: 'text-gray-600 hover:bg-gray-200 dark:text-gray-400 dark:hover:bg-gray-700'}"
169+
title={tool.name}
170+
>
171+
<tool.icon size={16} />
172+
</button>
173+
{/each}
174+
</div>
175+
</div>
176+
177+
<!-- Import/Export actions -->
178+
<div class="flex flex-wrap items-center gap-3">
179+
<span class="text-xs font-medium uppercase tracking-wide text-gray-600 dark:text-gray-300">
180+
Import / Export
181+
</span>
182+
<div class="h-4 w-px bg-gray-300 dark:bg-gray-600"></div>
183+
<div class="flex items-center gap-1.5">
184+
<button
185+
type="button"
186+
onclick={handleExport}
187+
disabled={annotationCount === 0}
188+
class="inline-flex items-center gap-1.5 rounded-md bg-emerald-500 px-2.5 py-1.5 text-xs font-medium text-white shadow-sm transition-all hover:bg-emerald-600 disabled:cursor-not-allowed disabled:opacity-50"
189+
>
190+
<Download size={14} />
191+
Export ({annotationCount})
192+
</button>
193+
<button
194+
type="button"
195+
onclick={handleClear}
196+
disabled={annotationCount === 0}
197+
class="inline-flex items-center gap-1.5 rounded-md bg-red-500 px-2.5 py-1.5 text-xs font-medium text-white shadow-sm transition-all hover:bg-red-600 disabled:cursor-not-allowed disabled:opacity-50"
198+
>
199+
<Trash2 size={14} />
200+
Clear All
201+
</button>
202+
<button
203+
type="button"
204+
onclick={handleImport}
205+
disabled={!hasExported}
206+
class="inline-flex items-center gap-1.5 rounded-md bg-blue-500 px-2.5 py-1.5 text-xs font-medium text-white shadow-sm transition-all hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-50"
207+
>
208+
<Upload size={14} />
209+
Import{hasExported && exported ? ` (${exported.length})` : ''}
210+
</button>
211+
</div>
212+
213+
{#if status}
214+
<span class="text-xs text-gray-500 dark:text-gray-400">{status}</span>
215+
{/if}
216+
</div>
217+
</div>
218+
219+
<!-- Viewer -->
220+
<div
221+
class="h-[500px] w-full overflow-hidden rounded-xl border border-gray-300 shadow-lg dark:border-gray-600"
222+
>
223+
<PDFViewer
224+
oninit={handleInit}
225+
onready={handleReady}
226+
config={{
227+
theme: { preference: themePreference },
228+
annotations: {
229+
annotationAuthor: 'Guest User',
230+
selectAfterCreate: true,
231+
},
232+
documentManager: {
233+
initialDocuments: [
234+
{
235+
url: 'https://snippet.embedpdf.com/ebook.pdf',
236+
documentId: 'import-export-doc',
237+
},
238+
],
239+
},
240+
}}
241+
style="width: 100%; height: 100%;"
242+
/>
243+
</div>
244+
</div>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './annotation-import-export-example.svelte';

0 commit comments

Comments
 (0)