API reference

ts
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)

ts
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.
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

ts
type DocumentSource =
  | { url: string; name?: string; fetchInit?: RequestInit }
  | { data: ArrayBuffer | Uint8Array | Blob; name?: string };
  • url is fetched by the wrapper in your page, so the usual rules apply: same origin, or a server that sends CORS headers. fetchInit is passed to fetch (headers, credentials, an abort signal). name defaults to the URL's last path segment.
  • data is copied, never detached: you can keep using your buffer. name defaults to the File's name, else document.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

ts
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.

ts
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

ts
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

ts
getPermissions(): Promise<Permissions>

What the open document's security allows. Needs a document.

getPageCount

ts
getPageCount(): Promise<number>

The number of pages. Needs a document.

isDirty

ts
isDirty(): Promise<boolean>

Whether the document has unsaved changes. Resolves to false when no document is open.

save

ts
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.

ts
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

ts
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

ts
exportXfdf(): Promise<string>

The document's comments as XFDF, with the author in each comment's title attribute. Needs a document.

getAnnotations

ts
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

ts
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

ts
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

ts
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

ts
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

ts
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

ts
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

ts
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

ts
setTheme(theme: "light" | "dark" | "system"): Promise<void>

Sets the colour theme. Rejects with BAD_REQUEST for any other value.

setLocale

ts
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

ts
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

ts
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

ts
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

ts
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

ts
on<K extends EventName>(name: K, listener: (data: EventMap[K]) => void): void
off<K extends EventName>(name: K, listener: (data: EventMap[K]) => void): void

Adds 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

ts
readonly iframe: HTMLIFrameElement

The 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.
ts
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:

ts
class FreePdfError extends Error {
  readonly code: ErrorCode;
}
ts
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

ts
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

ts
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.

ts
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.

ts
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.

ts
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

ts
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.