Vanilla JavaScript
Without a framework, call FreePdf.init(element, options). It mounts the viewer in element,
waits until it is ready (and until options.document is open), and resolves with the viewer's
API.
import { FreePdf } from "free-pdf-sdk";
const viewer = await FreePdf.init(document.getElementById("viewer")!, {
baseUrl: "/free-pdf-sdk/",
mode: "annotate",
document: { url: "/files/contract.pdf" },
});
viewer.on("annotationsChanged", ({ count }) => console.log(`${count} comments`));Get the files
There are two ways to get the SDK, depending on whether you use a bundler.
With a bundler (npm)
npm install free-pdf-sdk
npx free-pdf-sdk copy-viewer public/free-pdf-sdkImport FreePdf from free-pdf-sdk in your code. The second command copies the viewer files
(viewer/, version.json, the licences) into public/free-pdf-sdk; point baseUrl at the URL
that folder is served from. Run it again after every upgrade so the viewer matches the wrapper
(the wrapper logs a warning when the versions differ).
Without a bundler (release zip)
Download the release zip, unzip it, and serve the folder from your site, for example at
/free-pdf-sdk/. Besides viewer/ and version.json, it contains the wrapper as plain ES
modules, so a page can import it directly:
<script type="module">
import { FreePdf } from "/free-pdf-sdk/free-pdf-sdk.js";
// ...
</script>free-pdf-sdk.js (the core API) and element.js (the Web Component)
work without a build step. The React and Vue modules in the folder import react and vue, so
they need a bundler.
Same origin
Whichever way you choose, baseUrl must be on the same origin (scheme, host and port) as the
page. The viewer is an iframe loaded from that folder, and it talks to your page over
postMessage; init rejects with BAD_REQUEST if baseUrl is on another origin. The PDF itself
can come from anywhere your page can fetch.
The folder includes a large WebAssembly file (about 10 MB compressed). Serve .wasm files as
application/wasm, compressed if your server can; Hosting and security
covers headers, caching and Content-Security-Policy.
A complete page
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Contract</title>
<style>
body { margin: 0; display: flex; flex-direction: column; height: 100vh; font-family: system-ui, sans-serif; }
header { display: flex; gap: 12px; align-items: center; padding: 8px 16px; }
#viewer { flex: 1; min-height: 0; }
</style>
</head>
<body>
<header>
<span id="count">0 comments</span>
<button id="save" disabled>Save comments</button>
<span id="status"></span>
</header>
<div id="viewer"></div>
<script type="module">
import { FreePdf, FreePdfError } from "/free-pdf-sdk/free-pdf-sdk.js";
const status = document.getElementById("status");
try {
const viewer = await FreePdf.init(document.getElementById("viewer"), {
baseUrl: "/free-pdf-sdk/",
mode: "annotate",
author: "Priya Raman",
theme: "system",
disabledElements: ["download"],
document: { url: "/files/contract.pdf" },
listeners: {
annotationsChanged: ({ count }) => {
document.getElementById("count").textContent = `${count} comments`;
},
error: ({ code, message }) => console.error(code, message),
},
});
// Restore comments saved earlier.
const saved = await fetch("/api/contracts/42/annotations");
if (saved.ok) await viewer.importXfdf(await saved.text(), { replace: true });
const button = document.getElementById("save");
button.disabled = false;
button.addEventListener("click", async () => {
const xfdf = await viewer.exportXfdf();
await fetch("/api/contracts/42/annotations", {
method: "PUT",
headers: { "Content-Type": "application/vnd.adobe.xfdf" },
body: xfdf,
});
status.textContent = "Saved";
});
} catch (e) {
status.textContent = e instanceof FreePdfError ? `Could not open the viewer (${e.code})` : String(e);
}
</script>
</body>
</html>Size the container
The viewer is an iframe that fills element (width: 100%; height: 100%). Give the element a
size: a fixed height, flex: 1 in a flex column (as above), or a grid area.
Options
| Option | Type | Notes |
|---|---|---|
baseUrl |
string |
Required. URL of the viewer folder, on the page's origin. |
document |
{ url, name?, fetchInit? } or { data, name? } |
Opened once the viewer is ready. fetchInit is passed to fetch (headers, credentials). data is an ArrayBuffer, Uint8Array or Blob, and is copied. |
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 }. |
disabledElements |
ElementId[] |
Parts of the interface to hide, such as ["download", "print"]. |
author |
string |
Author of new comments; wins over a name saved in the viewer's settings. |
theme |
"light" | "dark" | "system" |
Default system. |
password |
string |
Opens a document that needs one, and lifts owner restrictions. |
locale |
string |
Interface language, such as en, de or zh-Hans; the closest available, else English. |
timeoutMs |
number |
How long a request waits for a reply before rejecting with TIMEOUT (default 60000; loads and saves get twice that). |
listeners |
{ [event]: listener } |
Attached before the viewer starts, so they hear ready and the first documentLoaded. |
After init, change the configuration with setMode, setFeatures, setUi,
setDisabledElements, setTheme, setAuthor and setLocale, and read the resolved
configuration with getConfig():
await viewer.setMode("review");
await viewer.setFeatures({ markup: false }); // replaces earlier overrides
await viewer.setUi({ quickToolbar: false }); // other ui options keep their value
const config = await viewer.getConfig();
console.log(config.mode, config.features.comments, config.readOnly);Listen to events
Pass listeners to init for events that may fire while init is still running, and use
on/off afterwards:
const onDirty = ({ dirty }: { dirty: boolean }) => {
document.title = dirty ? "• Contract" : "Contract";
};
viewer.on("dirtyChanged", onDirty);
// later
viewer.off("dirtyChanged", onDirty);| Event | Data | When |
|---|---|---|
ready |
{ sdk, pdfcraftRev } |
The viewer is configured. Listeners added later with on still receive it. |
documentLoaded |
{ pageCount, permissions } |
A document opened. |
documentError |
{ code, message } |
A document could not be opened. |
annotationsChanged |
{ count } |
Comments were added, changed or removed, by the user or the API. |
dirtyChanged |
{ dirty } |
The document gained or lost unsaved changes. |
error |
{ code, message } |
Something failed outside a request, for example a wrong password at load. |
saveRequested |
{ name, saveAs, data } |
The user pressed Save in the viewer. data is the PDF as a Uint8Array; the viewer now treats it as saved. |
Handle the viewer's Save button by uploading the PDF it hands you:
viewer.on("saveRequested", async ({ name, data }) => {
await fetch(`/api/files/${encodeURIComponent(name)}`, {
method: "PUT",
headers: { "Content-Type": "application/pdf" },
body: data,
});
});To remove the button instead, turn the save feature off: features: { save: false }.
Handle errors
Every method is async and rejects with a FreePdfError that has a code and a message
(English, meant for logs). init rejects (and removes the iframe) when the viewer can't start or
the first document can't be opened:
import { FreePdf, FreePdfError } from "free-pdf-sdk";
try {
const viewer = await FreePdf.init(element, { baseUrl: "/free-pdf-sdk/", document: { url } });
} catch (e) {
if (!(e instanceof FreePdfError)) throw e;
switch (e.code) {
case "TIMEOUT":
// The viewer did not start: is baseUrl right?
break;
case "INTERNAL":
// The viewer could not start, for example without WebGL.
break;
case "FETCH_FAILED":
// The PDF could not be downloaded (HTTP error, CORS, network).
break;
case "NEEDS_PASSWORD":
case "WRONG_PASSWORD":
// Ask for a password and init again with `password`.
break;
case "LOAD_FAILED":
// Not a PDF the viewer can open.
break;
default:
console.error(e.code, e.message);
}
}When documents may fail to open (user uploads, for example), init without a document and
call load afterwards. A failed load rejects, and the viewer stays (if the viewer itself
couldn't open the file, the document shown before is closed):
const viewer = await FreePdf.init(element, { baseUrl: "/free-pdf-sdk/", mode: "review" });
input.addEventListener("change", async () => {
const file = input.files?.[0];
if (!file) return;
try {
const { pageCount } = await viewer.load({ data: file });
console.log(`${file.name}: ${pageCount} pages`);
} catch (e) {
console.error(e);
}
});Destroy the viewer
destroy() removes the iframe and its listeners. Requests still waiting reject with
DESTROYED. It is safe to call twice.
await viewer.destroy();Call it when your page removes the container (closing a dialog, leaving a route in a single-page
app). Removing the container from the DOM without destroy() leaves the wrapper's message
listener behind.
Several viewers on one page
Call init once per container. Each viewer is its own iframe with its own channel, so events
and requests never cross between them:
<div class="pair">
<div id="original"></div>
<div id="revised"></div>
</div>
<style>
.pair { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; height: 90vh; }
</style>
<script type="module">
import { FreePdf } from "/free-pdf-sdk/free-pdf-sdk.js";
const [original, revised] = await Promise.all([
FreePdf.init(document.getElementById("original"), {
baseUrl: "/free-pdf-sdk/",
document: { url: "/files/contract-v1.pdf" },
}),
FreePdf.init(document.getElementById("revised"), {
baseUrl: "/free-pdf-sdk/",
mode: "review",
document: { url: "/files/contract-v2.pdf" },
}),
]);
revised.on("annotationsChanged", ({ count }) => console.log(`revised: ${count} comments`));
</script>Each viewer runs its own copy of the engine, so each one costs memory. The browser downloads the viewer files once and reuses them from its cache.
Things to know
- PDFs are processed in the browser with WebAssembly; the SDK does not upload them anywhere. Whatever you send to your server (XFDF, the saved PDF) is your code's choice.
- The viewer needs WebGL.
- Any configuration without an editing feature is read-only (
viewmode, for example).importXfdfanddeleteAllAnnotationsneed an annotation feature; without one they reject withREAD_ONLY. - Configuration lists every mode, feature and
uioption, and the API reference every method. - Prefer markup to code? The Web Component wraps all of this in
<free-pdf-viewer>.