FAQ and troubleshooting
The viewer is blank or never appears
Work through these in order; the browser's developer console usually names the cause.
- The container has no height. The viewer fills the element you mount it in. An element
with no height shows nothing, or only 200 px with the framework components. Give it an
explicit height (
height: 80vh,100dvh, or a flex child withflex: 1; min-height: 0). baseUrlis on another origin.initrejects withBAD_REQUESTand a message such as "the SDK must be served from this page's origin". Serve the viewer folder from your app's own origin; it can't be loaded from a CDN.baseUrlpoints at the wrong folder. It must be the folder that containsviewer/andversion.json. Open<baseUrl>/version.jsonin the browser: you should see the SDK version, not your app's HTML or a 404. A relativebaseUrlis resolved against the page's URL, so prefer one that starts with/.- The viewer files are missing or stale. Run
npx free-pdf-sdk copy-viewer <folder>again after every install or upgrade. A warning "the wrapper is X but the viewer is Y" in the console means the two don't match. - WebGL is unavailable. The viewer draws with WebGL. Open
chrome://gpuin Chrome or Edge, or try another browser; some virtual machines, remote desktops and locked-down machines disable it. Without WebGL the viewer can't start, andinitfails withINTERNAL. - A Content-Security-Policy blocks it. The console reports a CSP violation. The viewer
needs
'wasm-unsafe-eval'and its inline script and style allowed; Hosting and security shows a policy that works. - The
.wasmis served with the wrong type or not at all. Look in the Network tab for thefps-viewer-<hash>_bg.wasmrequest: it should be a 200 withapplication/wasm.
The right-hand rail looks crowded
The rail stacks a button per panel at the top and the zoom and page controls at the bottom. Below roughly 700 px of height the two groups overlap. Give the viewer more height, or trim the rail:
ui: {
zoomControls: false, // or pageNavigation: false
panels: { layers: false, attachments: false, tags: false },
}init fails with TIMEOUT
init waits 30 seconds for the viewer to start. When it doesn't, init rejects with
TIMEOUT and the message "the viewer did not start (is baseUrl right, and WebGL available?)".
The usual causes:
- The iframe loaded something other than the viewer: a wrong
baseUrlthat your single-page app answered with its ownindex.html. Make missing SDK files return 404 (see Hosting and security). - A CSP blocked the viewer's script or WebAssembly. (Missing WebGL usually gives
INTERNALinstead: the viewer loads but can't start.) - A very slow connection on the first visit, while the browser downloads the
.wasm(about 10 MB compressed). Serve it compressed and cached.
A method call fails with TIMEOUT
Each request waits timeoutMs (default 60 000 ms) for a reply; load and save get twice
that. A request that timed out is dropped by the viewer, so it never takes effect later.
Background tabs. Browsers slow down hidden tabs, and the viewer runs requests between frames. Retry when the tab is visible again (
document.visibilityState === "visible").Large documents. Raise
timeoutMsso loads and saves have time:await FreePdf.init(el, { baseUrl: "/free-pdf-sdk/", timeoutMs: 180_000, document: { url: "/files/drawings.pdf" } });
Large files
A PDF can be up to 512 MiB; larger files reject with
TOO_LARGE. In practice the browser's memory is the limit long before that, especially on phones.The whole document is held in memory: the wrapper reads it completely before handing it to the viewer. For big downloads, fetch it yourself to show progress, then pass the bytes:
const response = await fetch(url); const data = await response.arrayBuffer(); // or read response.body to report progress await viewer.load({ data, name: "drawings.pdf" });Only one load runs at a time; a second
loadwhile one is running rejects withBUSY.An XFDF passed to
importXfdfcan be up to 32 MiB.
Does it work on phones and tablets?
The viewer runs in mobile browsers that support WebAssembly and WebGL, in the same iframe as on the desktop. Keep in mind:
- The first visit downloads about 10 MB (compressed); cache headers make later visits load from the browser cache.
- Memory is tighter than on desktops, so very large PDFs may fail to open.
- Give the container a height that fits the screen, such as
height: 100dvh, and consideruioptions (for examplequickToolbar: falseorrightRail: false) or a simpler mode to leave more room for the page. See Configuration.
Test on the devices your users have.
Next.js, Nuxt, SvelteKit, Angular SSR
The viewer needs window, an iframe and WebGL, so it can only be mounted in the browser.
Next.js: use the React component in a client component (
"use client"at the top of the file). The component mounts the viewer in an effect, so it never runs during server rendering."use client"; import { FreePdfViewer } from "free-pdf-sdk/react"; export default function Viewer({ url }: { url: string }) { return <FreePdfViewer baseUrl="/free-pdf-sdk/" document={{ url }} style={{ height: "80vh" }} />; }Put the viewer folder in
public/free-pdf-sdk.Nuxt: wrap the Vue component in
<ClientOnly>, and put the folder inpublic/.SvelteKit: works with server rendering on; actions only run in the browser. See Svelte. Put the folder in
static/.Angular with SSR, or any server-rendered page using the Web Component: import
free-pdf-sdk/elementonly in the browser, for example with a dynamicimport()after checkingisPlatformBrowser, because the element class needs browser globals when the module loads.
Can I show several viewers on one page?
Yes. Each FreePdf.init creates its own iframe with its own private message channel, so viewers
never interfere with each other. Each one loads its own copy of the viewer in memory, so keep the
number reasonable on phones, and call destroy() (or unmount the component) when you no longer
need one.
Can I load the viewer from a CDN?
No. The viewer folder must be served from the same origin as your page. The SDK has no CDN build and no server component: you serve the static files yourself. The reasons are explained in Hosting and security.
Where do my documents go?
Nowhere. Your page fetches the PDF (or passes its bytes), the viewer opens it with WebAssembly
in the browser, and the result of save(), exportXfdf() or the saveRequested event goes to
your code. The SDK sends no documents, comments, passwords or telemetry to any server. The viewer
keeps its own preferences in the browser's storage for your origin.
The viewer is read-only, but I passed annotations
The default mode is view. Choose a mode that edits, such as annotate. Any configuration with
no editing feature is read-only, and in that state importXfdf and deleteAllAnnotations
reject with READ_ONLY. Check what the viewer resolved:
const config = await viewer.getConfig();
console.log(config.mode, config.readOnly, config.features);If the configuration allows comments but you get NOT_PERMITTED, the document's own security
forbids them; see Encrypted PDFs.
My PDF URL fails with FETCH_FAILED
The wrapper fetches document.url from your page. A PDF on another origin needs CORS headers
that allow your origin; private files may need fetchInit with credentials or headers:
await viewer.load({
url: "https://files.example.com/contract.pdf",
fetchInit: { credentials: "include" },
});An HTTP error status also gives FETCH_FAILED, with the status in the message.
Can I open a password-protected PDF?
Yes. Pass password to init or load, and lift owner restrictions with unlock. See
Encrypted PDFs.
What does it not do?
Free PDF SDK has no server component, no hosted or CDN version, no document comparison and no colour separation preview. The full list of what it offers is in Configuration.
What is the licence?
Free PDF SDK is licensed MIT OR Apache-2.0, at your option, including commercial use and
white-labelling. The viewer embeds open-source components (it is built on the pdfcraft engine);
their licences and notices are in NOTICE and third-party/, which copy-viewer copies next to
the viewer. Keep those files when you deploy or redistribute it.
Does the viewer support PDF layers?
Yes. Documents with optional content (layers such as structure, furniture, electrical, dimensions or revisions)
get a Layers panel on the right-hand rail. Each layer has a show/hide toggle, the page redraws
immediately, and printing follows what is shown. Toggling a layer does not change the file. Hide
the panel with ui: { panels: { layers: false } } if you don't want it; the sample document in
the live demo has four layers to try.