Vue
free-pdf-sdk/vue exports FreePdfViewer, a Vue 3 component. Props configure the viewer,
emitted events report what happens, and a template ref gives you the full API.
<script setup lang="ts">
import { FreePdfViewer } from "free-pdf-sdk/vue";
</script>
<template>
<FreePdfViewer base-url="/free-pdf-sdk/" :document="{ url: '/files/contract.pdf' }" style="height: 80vh" />
</template>Install
npm install free-pdf-sdk
npx free-pdf-sdk copy-viewer public/free-pdf-sdkThe second command copies the viewer files (viewer/, version.json, the licences) into
public/free-pdf-sdk, which Vite and Nuxt serve at /free-pdf-sdk/. That URL is baseUrl, and
it must be on the same origin as your app. Run the command again after every upgrade, for example
from a postinstall script:
{
"scripts": {
"postinstall": "free-pdf-sdk copy-viewer public/free-pdf-sdk"
}
}The component needs Vue 3.3 or later.
Props
| Prop | Type | Notes |
|---|---|---|
base-url |
string |
Required. URL of the viewer folder, on the page's origin. |
document |
{ url, name?, fetchInit? } or { data, name? } |
The PDF to open. data is an ArrayBuffer, Uint8Array or Blob. |
mode |
Mode |
view (default, read-only), review, annotate, fill, sign, organize, edit or full. |
features |
FeatureFlags |
Feature switches on top of the mode, such as { stamps: false }. |
ui |
UiOptions |
Interface options, such as { toolsPanel: false }. |
disabled-elements |
ElementId[] |
Parts of the interface to hide, such as ['download', 'print']. |
theme |
'light' | 'dark' | 'system' |
Default system. |
author |
string |
Author of new comments. |
locale |
string |
Interface language, such as en, de or zh-Hans. |
password |
string |
Used when document is opened: opens a protected file and lifts owner restrictions. |
timeout-ms |
number |
How long requests wait for a reply (default 60000). Read once, at mount. |
Props can be written in kebab-case or camelCase (base-url or baseUrl).
Size
The component renders a div with position: relative, width: 100%, height: 100% and a
min-height of 200px; the viewer fills it. Either give the parent a height, or set one on the
component: class and style fall through to the div.
<FreePdfViewer class="viewer" base-url="/free-pdf-sdk/" :document="doc" />
<style scoped>
.viewer { height: calc(100vh - 64px); }
</style>Events
The component emits every viewer event. Listen in kebab-case:
| Event | Payload | When |
|---|---|---|
@ready |
{ sdk, pdfcraftRev } |
The viewer is configured and ready for requests. |
@document-loaded |
{ pageCount, permissions } |
A document opened. |
@document-error |
{ code, message } |
A document could not be opened. |
@annotations-changed |
{ count } |
Comments were added, changed or removed, by the user or the API. |
@dirty-changed |
{ dirty } |
The document gained or lost unsaved changes. |
@error |
{ code, message } |
Something failed outside a request: the viewer could not start, a prop change was refused. |
@save-requested |
{ name, saveAs, data } |
The user pressed Save in the viewer. data is the PDF as a Uint8Array; the viewer now treats it as saved. |
<FreePdfViewer
base-url="/free-pdf-sdk/"
mode="annotate"
:document="{ url: '/files/contract.pdf' }"
@document-loaded="({ pageCount }) => (pages = pageCount)"
@annotations-changed="({ count }) => (comments = count)"
@error="({ code, message }) => console.error(code, message)"
/>Call the API with a template ref
The component exposes two methods:
viewer()returns the viewer's API once it is ready, elsenull.whenReady()resolves with the API once the viewer is ready and its first document has loaded (or failed to).
<script setup lang="ts">
import { ref } from "vue";
import type { FreePdfViewer as Api } from "free-pdf-sdk";
import { FreePdfViewer } from "free-pdf-sdk/vue";
type ViewerHandle = { viewer(): Api | null; whenReady(): Promise<Api> };
const viewerRef = ref<ViewerHandle | null>(null);
async function firstPage() {
const viewer = await viewerRef.value?.whenReady();
await viewer?.goToPage(1);
}
</script>
<template>
<button @click="firstPage">First page</button>
<FreePdfViewer ref="viewerRef" base-url="/free-pdf-sdk/" :document="{ url: '/files/report.pdf' }" style="height: 80vh" />
</template>Every API method is async and rejects with a FreePdfError that has a code and a message
(import FreePdfError from free-pdf-sdk). Don't store the API object in reactive state; keep it
in a local variable, or use markRaw if you must.
Reactive props
Props are watched deeply. A change calls the matching API method on the running viewer, in this
order: setMode, setFeatures, setUi, setDisabledElements, setTheme, setAuthor,
setLocale, then load for a new document. A refused change is reported through @error.
Reactive objects are copied to plain objects before they are sent to the viewer, so you can pass
reactive() state directly.
<script setup lang="ts">
import { reactive, ref } from "vue";
import type { Mode } from "free-pdf-sdk";
import { FreePdfViewer } from "free-pdf-sdk/vue";
const mode = ref<Mode>("review");
const features = reactive({ stamps: false, measurement: false });
const ui = reactive({ quickToolbar: true, panels: { bookmarks: false } });
const dark = ref(false);
</script>
<template>
<label><input type="checkbox" v-model="features.stamps" /> Stamps</label>
<label><input type="checkbox" v-model="ui.quickToolbar" /> Quick toolbar</label>
<label><input type="checkbox" v-model="dark" /> Dark</label>
<select v-model="mode">
<option value="view">View</option>
<option value="review">Review</option>
<option value="annotate">Annotate</option>
</select>
<FreePdfViewer
base-url="/free-pdf-sdk/"
:mode="mode"
:features="features"
:ui="ui"
:theme="dark ? 'dark' : 'light'"
:document="{ url: '/files/contract.pdf' }"
style="height: 80vh"
/>
</template>How changes are detected:
features,uianddisabled-elementsare compared by value.documentwith aurlis compared byurlandname; withdata, by the identity of the bytes. Keep the sameArrayBuffer,Uint8ArrayorBlobto avoid reloads.featuresreplaces the overrides: clearing the prop goes back to the mode's own features.uioptions you leave out keep their value, and clearingmode,theme,authororlocalekeeps the current value.passwordis only used when a document is opened; change it together withdocument.- Changing
base-urldestroys the viewer and mounts a new one.
Calls you make through the API don't change the props. If you call viewer.setMode() yourself,
keep your mode prop in step.
Example: annotate and save comments as XFDF
<script setup lang="ts">
import { ref } from "vue";
import type { FreePdfViewer as Api } from "free-pdf-sdk";
import { FreePdfViewer } from "free-pdf-sdk/vue";
const props = defineProps<{ contractId: string; userName: string }>();
const viewerRef = ref<{ viewer(): Api | null; whenReady(): Promise<Api> } | null>(null);
const count = ref(0);
const status = ref("");
async function restore() {
const viewer = await viewerRef.value?.whenReady();
if (!viewer) return;
const res = await fetch(`/api/contracts/${props.contractId}/annotations`);
if (res.ok) await viewer.importXfdf(await res.text(), { replace: true });
}
async function save() {
const viewer = viewerRef.value?.viewer();
if (!viewer) return;
const xfdf = await viewer.exportXfdf();
await fetch(`/api/contracts/${props.contractId}/annotations`, {
method: "PUT",
headers: { "Content-Type": "application/vnd.adobe.xfdf" },
body: xfdf,
});
status.value = "Saved";
}
async function upload({ data }: { data: Uint8Array }) {
await fetch(`/api/contracts/${props.contractId}/pdf`, { method: "PUT", body: data });
status.value = "PDF saved";
}
</script>
<template>
<div class="page">
<header>
{{ count }} comments
<button @click="save">Save comments</button>
{{ status }}
</header>
<FreePdfViewer
ref="viewerRef"
class="viewer"
base-url="/free-pdf-sdk/"
mode="annotate"
:author="userName"
:disabled-elements="['download']"
:document="{ url: `/api/contracts/${contractId}/pdf` }"
@document-loaded="restore"
@annotations-changed="({ count: n }) => (count = n)"
@save-requested="upload"
@error="({ code, message }) => (status = `${code}: ${message}`)"
/>
</div>
</template>
<style scoped>
.page { display: flex; flex-direction: column; height: 100vh; }
.viewer { flex: 1; }
</style>@document-loaded can fire before viewer() is set for the first document, so restore waits
with whenReady(). In annotate mode comments can be imported; without an annotation feature
(view or organize mode, say) importXfdf rejects with READ_ONLY.
Nuxt
The viewer runs only in the browser. Wrap the component in <ClientOnly>:
<!-- pages/contracts/[id].vue -->
<script setup lang="ts">
import { FreePdfViewer } from "free-pdf-sdk/vue";
const route = useRoute();
</script>
<template>
<ClientOnly>
<FreePdfViewer
base-url="/free-pdf-sdk/"
mode="review"
:document="{ url: `/api/contracts/${route.params.id}/pdf` }"
style="height: calc(100vh - 64px)"
/>
<template #fallback>
<p>Loading viewer…</p>
</template>
</ClientOnly>
</template>Copy the viewer into Nuxt's public folder (public/ in Nuxt 3 and 4):
npx free-pdf-sdk copy-viewer public/free-pdf-sdkIf the app is served under a base URL (app.baseURL), include it in base-url.
Things to know
- Unmounting the component destroys the viewer; pending calls reject with
DESTROYED. - If a document can't be opened,
@document-errorreports why and the viewer keeps running: set anotherdocumentto try again. - The viewer is an iframe on your origin that talks to the page over
postMessage. PDFs are processed in the browser with WebAssembly and are not uploaded anywhere by the SDK. It needs WebGL; if it can't start,@errorreportsTIMEOUT(usually a wrongbase-url) orINTERNAL(for example without WebGL). - See Vanilla JavaScript for the core
FreePdf.initAPI the component wraps.