API reference
import { FreePdf, FreePdfError } from "free-pdf-sdk";
const viewer = await FreePdf.init(document.getElementById("viewer")!, {
baseUrl: "/free-pdf-sdk/",
document: { url: "/files/contract.pdf" },
mode: "annotate",
features: { stamps: false },
author: "Priya Raman",
theme: "system",
listeners: {
documentLoaded: ({ pageCount }) => console.log(`${pageCount} pages`),
},
});
viewer.on("annotationsChanged", ({ count }) => console.log(`${count} comments`));
const xfdf = await viewer.exportXfdf();The package free-pdf-sdk exports FreePdf (also the default export), init,
FreePdfError, the constants SDK_VERSION, PROTOCOL_VERSION, MODES and FEATURES, and
every type on this page. The TypeScript declarations are the authoritative reference; this page
describes them.
Each viewer is an iframe on your page's own origin. The wrapper talks to it over a versioned
postMessage protocol, so every method is async and rejects with a FreePdfError.
FreePdf.init(element, options)
function init(element: HTMLElement, options: InitOptions): Promise<FreePdfViewer>;Mounts an iframe that fills element, waits for the viewer to start, applies the options,
opens options.document if one is given, and resolves to a FreePdfViewer. Give element a
size: the viewer fills it, and an element with no height shows nothing.
If anything fails (the viewer doesn't start, the options are invalid, the document can't be
opened) the promise rejects with a FreePdfError and the iframe is removed.
| Rejects with | When |
|---|---|
BAD_REQUEST |
element is not an HTMLElement, baseUrl is missing, baseUrl is on another origin, or an option is invalid (unknown mode, feature, ui option, panel or element id; empty author...). |
TIMEOUT |
The viewer did not start within 30 seconds: usually a wrong baseUrl (the iframe loaded something other than the viewer). |
INTERNAL |
The viewer page loaded but could not start, for example because WebGL is unavailable. |
UNSUPPORTED_PROTOCOL |
The viewer files speak another protocol version than the wrapper. Serve the viewer files from the same release as the package. |
any load error |
options.document could not be opened (see load). |
If the wrapper and viewer versions differ but speak the same protocol, the viewer still starts
and a warning is logged to the console. Run npx free-pdf-sdk copy-viewer <folder> after every
upgrade so they match.
InitOptions
| Option | Type | Default | Description |
|---|---|---|---|
baseUrl |
string |
required | URL of the SDK folder (the one holding viewer/ and version.json). It must be on the page's origin. A trailing / is added if missing; relative URLs resolve against the page. |
document |
DocumentSource |
none | A PDF to open once the viewer is ready. |
mode |
Mode |
"view" |
A preset of features. view is read-only. See Configuration. |
features |
FeatureFlags |
{} |
Turn individual features on or off, on top of the mode. |
ui |
UiOptions |
all defaults | Show or hide parts of the interface. |
disabledElements |
ElementId[] |
[] |
Single commands and tools to hide. |
author |
string |
the name saved in the viewer's preferences | Author of new comments (written to XFDF title). Wins over the saved name. |
theme |
"light" | "dark" | "system" |
"system" |
Colour theme; system follows the browser. |
password |
string |
none | Opens a document that needs a password, and lifts owner restrictions (as unlock). Used with document. |
locale |
string |
the browser's language | Interface language such as en, de or zh-Hans; the closest available is used, English otherwise. |
loader |
string | LoaderOptions | false |
a built-in spinner | The loading animation shown while the viewer starts and while a document opens. A string is an image URL. false shows nothing. See Loading animation. |
timeoutMs |
number |
60000 |
How long to wait for a reply before rejecting with TIMEOUT. Loads (including fetching a URL) and save get twice this. |
listeners |
{ [event]: listener } |
none | Listeners attached before the viewer starts, so they also hear events fired while init is running (ready, the first documentLoaded). |
DocumentSource
type DocumentSource =
| { url: string; name?: string; fetchInit?: RequestInit }
| { data: ArrayBuffer | Uint8Array | Blob; name?: string };urlis fetched by the wrapper in your page, so the usual rules apply: same origin, or a server that sends CORS headers.fetchInitis passed tofetch(headers, credentials, an abortsignal).namedefaults to the URL's last path segment.datais copied, never detached: you can keep using your buffer.namedefaults to theFile's name, elsedocument.pdf.
The document is read in the browser and handed to the viewer in the iframe. Nothing is uploaded anywhere by the SDK.
FreePdfViewer methods
Every method returns a promise. Methods marked needs a document reject with NO_DOCUMENT
when no document is open, and with BUSY while a document is loading. Any method can also
reject with TIMEOUT (no reply within timeoutMs) or DESTROYED (the viewer was destroyed).
Documents
load
load(document: DocumentSource, options?: { password?: string }): Promise<LoadResult>Opens a document, replacing the one shown. Resolves to { pageCount, permissions, unlocked }
once it is open, and fires documentLoaded.
password is tried in two ways: if the document needs a password to open, it is used to open
it; otherwise, if the document has owner restrictions, it is tried as the owner password.
unlocked is true when it lifted those restrictions. A wrong owner password does not fail
the load: the document opens restricted, unlocked is false and an error event with
WRONG_PASSWORD fires.
| Rejects with | When |
|---|---|
FETCH_FAILED |
A url could not be fetched (network error, HTTP error status, or no response within twice timeoutMs). |
NEEDS_PASSWORD |
The document needs a password to open and none was given. |
WRONG_PASSWORD |
The password given does not open it. |
LOAD_FAILED |
The file is not a PDF the viewer can open. |
TOO_LARGE |
The PDF is larger than 512 MiB. |
BAD_REQUEST |
The PDF is empty, or the source has neither url nor data. |
BUSY |
Another document is still loading. |
A failed fetch (FETCH_FAILED, or BAD_REQUEST for a source with neither url nor data) and
a file the viewer can't open (LOAD_FAILED, NEEDS_PASSWORD, WRONG_PASSWORD) also fire
documentError. When the viewer itself can't open the file, the document shown before is
already closed, so no document is open afterwards; a failed fetch leaves it open.
try {
await viewer.load({ url: "/files/protected.pdf" });
} catch (e) {
if (e instanceof FreePdfError && e.code === "NEEDS_PASSWORD") {
await viewer.load({ url: "/files/protected.pdf" }, { password: prompt("Password") ?? "" });
} else {
throw e;
}
}unlock
unlock(password: string): Promise<Permissions>Lifts the open document's owner restrictions with its owner password and resolves to the new
permissions. It changes nothing in the file: a PDF saved afterwards keeps its protection. If
the document has no restrictions to lift, it resolves to the current permissions.
Needs a document. Rejects with WRONG_PASSWORD.
getPermissions
getPermissions(): Promise<Permissions>What the open document's security allows. Needs a document.
getPageCount
getPageCount(): Promise<number>The number of pages. Needs a document.
isDirty
isDirty(): Promise<boolean>Whether the document has unsaved changes. Resolves to false when no document is open.
save
save(): Promise<Uint8Array>The PDF with every change, comments included. The viewer then treats the document as saved:
isDirty() becomes false and dirtyChanged fires. It works in every mode, whether or not the
save feature (the viewer's own Save button) is on. Needs a document.
const pdf = await viewer.save();
await fetch("/api/files/contract.pdf", { method: "PUT", body: pdf });Annotations
See Annotations and XFDF for a full guide.
importXfdf
importXfdf(xfdf: string | Blob, options?: { replace?: boolean }): Promise<number>Imports comments from standard XFDF, whichever PDF tool wrote it. With replace: true the existing comments are
deleted first; nothing is deleted when the XFDF is invalid. Resolves to the number of top-level
comments imported (replies are not counted; comments on pages the document doesn't have are
skipped). An XFDF with no comments imports 0. Needs a document.
| Rejects with | When |
|---|---|
READ_ONLY |
No annotation feature is on (for example view or organize mode), or the document can't be edited. |
NOT_PERMITTED |
The document's security doesn't allow comments; unlock it with the owner password. |
XFDF_INVALID |
The XFDF can't be read, or it holds form field values (it imports comments only). |
TOO_LARGE |
The XFDF is larger than 32 MiB. |
exportXfdf
exportXfdf(): Promise<string>The document's comments as XFDF, with the author in each comment's title attribute.
Needs a document.
getAnnotations
getAnnotations(): Promise<Annotation[]>The document's top-level comments, each with its replies. The array's length is the exact
number of comments: it counts what exportXfdf() writes. Needs a document.
deleteAllAnnotations
deleteAllAnnotations(): Promise<number>Deletes every comment, locked ones included, and resolves to how many there were.
Needs a document. Rejects with READ_ONLY (no annotation feature is on, or the document
can't be edited) or NOT_PERMITTED (the document's security doesn't allow comments).
setAuthor
setAuthor(author: string): Promise<void>Sets the author of new comments. The name is trimmed and control characters are removed; it
stays in effect for documents loaded later. Rejects with BAD_REQUEST when empty and
TOO_LARGE over 200 characters.
Configuration
See Configuration for what each option does.
setMode
setMode(mode: Mode): Promise<void>Switches mode. Your feature overrides still apply on top of it. Rejects with BAD_REQUEST for
an unknown mode.
setFeatures
setFeatures(features: FeatureFlags): Promise<void>Replaces the feature overrides (relative to the mode). Pass {} to use the mode's features as
they are. Rejects with BAD_REQUEST for an unknown feature or a value that isn't a boolean.
setUi
setUi(ui: UiOptions): Promise<void>Changes interface options; options you leave out keep their value, and panels you don't name
keep theirs. Rejects with BAD_REQUEST for an unknown option or panel.
setDisabledElements
setDisabledElements(ids: ElementId[]): Promise<void>Hides parts of the interface, replacing the previous list. Rejects with BAD_REQUEST for an
unknown id and TOO_LARGE for more than 256 ids.
getConfig
getConfig(): Promise<ViewerConfig>The configuration in effect: the mode, every feature on or off, the interface options, the hidden elements and whether the viewer is read-only.
Display
setTheme
setTheme(theme: "light" | "dark" | "system"): Promise<void>Sets the colour theme. Rejects with BAD_REQUEST for any other value.
setLocale
setLocale(locale: string): Promise<string>Sets the interface language and resolves to the language actually used: the exact tag if
available, else its primary language (de for de-AT), else en. Rejects with
BAD_REQUEST if locale is not a language tag (letters, digits, - and _).
goToPage
goToPage(page: number): Promise<void>Shows page page, counted from 1. Needs a document. Rejects with BAD_REQUEST when
page is not a whole number from 1 to the page count.
setZoom
setZoom(zoom: number | "fit-width" | "fit-page"): Promise<void>Sets the zoom: a percentage from 10 to 6400, or a fit. Needs a document. Rejects with
BAD_REQUEST outside that range.
Lifecycle
version
version(): Promise<VersionInfo>Resolves to { sdk, pdfcraftRev }: the SDK version of the viewer files and the engine
revision they were built from. FreePdf.version is the wrapper's own version, available
without a viewer.
destroy
destroy(): Promise<void>Removes the iframe and every listener. Pending requests reject with DESTROYED, and so does
every later call. Safe to call twice. Call it when your component unmounts.
on / off
on<K extends EventName>(name: K, listener: (data: EventMap[K]) => void): void
off<K extends EventName>(name: K, listener: (data: EventMap[K]) => void): voidAdds or removes an event listener. A listener that throws is logged and does not stop the
others. A ready listener added after the viewer is ready is still called once.
iframe
readonly iframe: HTMLIFrameElementThe iframe holding the viewer, for example to focus it.
Events
| Event | Data | When |
|---|---|---|
ready |
{ sdk: string; pdfcraftRev: string } |
The viewer is configured and ready for requests. Fires before the first document opens. |
documentLoaded |
{ pageCount: number; permissions: Permissions } |
A document opened. |
documentError |
{ code: ErrorCode; message: string } |
A document could not be opened (including a failed fetch). |
annotationsChanged |
{ count: number } |
Comments were added, changed or removed, by the user or the API. count is the number of top-level comments. |
dirtyChanged |
{ dirty: boolean } |
The document gained or lost unsaved changes. |
error |
{ code: ErrorCode; message: string } |
Something failed outside a request, for example a wrong owner password at load (the document opens restricted). |
saveRequested |
{ name: string; saveAs: boolean; data: Uint8Array } |
The user pressed Save (or Save As) in the viewer. data is the saved PDF; the viewer treats the document as saved. Store it yourself. |
viewer.on("dirtyChanged", ({ dirty }) => {
saveButton.disabled = !dirty;
});
viewer.on("saveRequested", async ({ name, data }) => {
await fetch(`/api/files/${encodeURIComponent(name)}`, { method: "PUT", body: data });
});ready and the first documentLoaded fire while init is still running. To hear them, pass
the listeners in options.listeners (a ready listener added later with on is still called).
Errors
Every failure is a FreePdfError, a subclass of Error with a machine-readable code:
class FreePdfError extends Error {
readonly code: ErrorCode;
}try {
await viewer.importXfdf(xfdf, { replace: true });
} catch (e) {
if (!(e instanceof FreePdfError)) throw e;
switch (e.code) {
case "NOT_PERMITTED":
showMessage("This file is protected. Unlock it to add comments.");
break;
case "XFDF_INVALID":
showMessage("The stored comments could not be read.");
break;
default:
console.error(e.code, e.message);
}
}Messages are in English and meant for logs, not for users: switch on code.
| Code | Meaning |
|---|---|
BAD_REQUEST |
A parameter is missing, mistyped or out of range, or a name (mode, feature, ui option, panel, element id) is unknown. |
UNKNOWN_METHOD |
The viewer has no such method (the wrapper and viewer are from different releases). |
UNSUPPORTED_PROTOCOL |
The viewer speaks another protocol version than the wrapper. |
NOT_READY |
The viewer's iframe is not attached to the page. |
NO_DOCUMENT |
No document is open. |
LOAD_FAILED |
The document could not be opened. |
NEEDS_PASSWORD |
The document needs a password to open. |
WRONG_PASSWORD |
The password is wrong. |
NOT_PERMITTED |
The document's security settings don't allow this; unlock it with the owner password. |
READ_ONLY |
No annotation feature is on (for example in view mode), or the document can't be edited. |
XFDF_INVALID |
The XFDF could not be read, or it holds form field values. |
TOO_LARGE |
A value is larger than the protocol allows (see Limits). |
BUSY |
A document is loading, or the request expired before the viewer could run it. |
INTERNAL |
Something failed inside the viewer, or the viewer could not start (for example without WebGL). |
TIMEOUT |
From the wrapper: no reply within timeoutMs, or the viewer did not start within 30 seconds. |
DESTROYED |
From the wrapper: the viewer was destroyed. |
FETCH_FAILED |
From the wrapper: a document URL could not be fetched. |
A request that times out in the wrapper never takes effect later: every request carries a
deadline, and the viewer drops it (with BUSY) if it reaches the viewer too late, as can
happen in a background tab.
Types
Mode, Feature, FeatureFlags
type Mode = "view" | "review" | "annotate" | "fill" | "sign" | "organize" | "edit" | "full";
type Feature =
| "comments" | "markup" | "shapes" | "ink" | "stamps" | "measurement"
| "fillSign" | "formFill" | "formDesign" | "digitalSignatures" | "signaturePanel"
| "pages" | "bookmarks" | "editContent" | "flatten" | "redaction"
| "accessibility" | "standards" | "javascript" | "security" | "history"
| "save" | "download" | "export" | "print" | "search" | "copy"
| "documentInfo" | "help" | "palette" | "preferences";
type FeatureFlags = Partial<Record<Feature, boolean>>;MODES and FEATURES export the same lists as arrays. What each mode and feature means is in
Configuration.
UiOptions and PanelName
interface UiOptions {
toolsPanel?: boolean; // default true
toolsRail?: boolean; // default true
quickToolbar?: boolean; // default true
rightRail?: boolean; // default true
zoomControls?: boolean; // default true
pageNavigation?: boolean; // default true
menu?: boolean; // default true
searchBox?: boolean; // default true (needs the palette feature)
panels?: Partial<Record<PanelName, boolean>>;
lockLayout?: boolean; // default false
showPlannedTools?: boolean; // default false
externalLinks?: boolean; // default true
}
type PanelName =
| "comments" | "bookmarks" | "pages" | "fields" | "layers"
| "attachments" | "signatures" | "accessibility" | "tags" | "search";ElementId
The ids disabledElements accepts: download, save, print, open, flatten,
settings, import, and the tools tool:note, tool:freetext, tool:callout,
tool:highlight, tool:underline, tool:strikeout, tool:squiggly, tool:caret,
tool:replace, tool:ink, tool:line, tool:arrow, tool:square, tool:circle,
tool:polygon, tool:polyline, tool:cloud, tool:attach, tool:stamp, tool:image,
tool:link, tool:measure, tool:fillsign, tool:signature. What each one hides is in
Configuration.
ViewerConfig
What getConfig() resolves to.
interface ViewerConfig {
mode: Mode;
features: Record<Feature, boolean>; // every feature: the mode's preset with your overrides
ui: Required<Omit<UiOptions, "panels">> & { panels: Partial<Record<PanelName, boolean>> };
disabledElements: ElementId[];
readOnly: boolean; // no feature that changes the document is on
}Permissions
What the open document's security allows.
interface Permissions {
canAnnotate: boolean; // comments may be added, changed, deleted and imported
canPrint: boolean;
canCopy: boolean;
isEncrypted: boolean;
canUnlock: boolean; // an owner password would lift restrictions (unlock)
}canAnnotate is true only when the document allows both adding comments and modifying it,
because importing comments needs both. A file protected with an owner password that forbids
commenting reports false until you call unlock(ownerPassword).
Annotation
What getAnnotations() lists.
interface Annotation {
id: string; // XFDF name
type: string; // text (note), freetext, square, circle, line, ink, polygon,
// polyline, highlight, underline, squiggly, strikeout, stamp,
// caret, fileattachment, sound, redact
author: string | null; // XFDF title
subject: string | null;
contents: string | null; // plain text
date: string | null; // ISO 8601: last modified, else created
pageIndex: number; // 0-based
color: string | null; // "#RRGGBB"
replies: AnnotationReply[]; // oldest first
}
interface AnnotationReply {
id: string;
author: string | null;
contents: string | null;
date: string | null;
}type is the PDF annotation subtype in lowercase.
Other types
interface DocumentInfo { pageCount: number; permissions: Permissions } // documentLoaded
interface LoadResult extends DocumentInfo { unlocked: boolean } // load()
interface VersionInfo { sdk: string; pdfcraftRev: string } // ready, version()
interface ErrorInfo { code: ErrorCode; message: string } // documentError, error
type Theme = "light" | "dark" | "system";
type Zoom = number | "fit-width" | "fit-page";
type EventName = keyof EventMap;
type Listener<K extends EventName> = (data: EventMap[K]) => void;Limits
| Value | Limit |
|---|---|
| PDF size | 512 MiB (TOO_LARGE) |
| XFDF size | 32 MiB (TOO_LARGE) |
| Document name | 512 characters; path separators and control characters are removed |
| Author | 200 characters, not empty |
| Password | 1024 characters |
| Locale tag | 35 characters |
disabledElements |
256 ids |
| Zoom | 10 to 6400 percent |
| Page number | from 1 to the page count |
| Viewer start | 30 seconds (TIMEOUT) |
| Request reply | timeoutMs (default 60 seconds); loads and save twice that |
Only one document is open per viewer, and one load runs at a time. To show two documents at once, mount two viewers.
Requirements
- The SDK folder (
baseUrl) is served from the page's own origin. The viewer runs in an iframe on that origin and only talks to its parent there. - The browser supports WebAssembly and WebGL.
- Documents are processed in the browser; the SDK has no server component and uploads nothing.