ScreenshotNeo

BlogHTML to image & PDF

How to Embed PDF.js in HTML with a Working Example

Embed PDF.js in HTML with a complete working example, correct worker setup, CORS guidance, high-DPI rendering, troubleshooting, and production tips.

By the ScreenshotNeo team1 October 20268 min read

PDF.js lets a web page parse and render PDF files in the browser. For a small custom embed, load the display library, point it to the matching worker, call getDocument(), retrieve a page, create a viewport, size a canvas, and call page.render(). Serve the page over HTTP or HTTPS; opening it as file:// prevents the worker from running.

This guide uses the browser distribution from pdfjs-dist. Mozilla’s Getting Started page currently shows stable release 6.3.289; use the version installed in your project and keep the display library and worker on that same version. See Mozilla’s PDF.js Getting Started guide and the official examples.

Working PDF.js HTML example

Create a project, install PDF.js, place a PDF at public/files/example.pdf, and serve the project with a local HTTP server. The import paths below assume your bundler or static server exposes node_modules; adjust them if your build copies assets elsewhere.

npm install pdfjs-dist

# One simple static server option
npx http-server .
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>PDF.js example</title>
  <style>
    body { margin: 2rem; font: 16px system-ui, sans-serif; }
    #pdf-page { display: block; max-width: 100%; height: auto; }
    #error { color: #b00020; }
  </style>
</head>
<body>
  <h1>PDF preview</h1>
  <canvas id="pdf-page" aria-label="Page 1 of the PDF"></canvas>
  <p id="error" role="alert"></p>

  <script type="module">
    import * as pdfjsLib from "/node_modules/pdfjs-dist/build/pdf.mjs";

    // The worker must come from the same PDF.js release as the imported module.
    pdfjsLib.GlobalWorkerOptions.workerSrc =
      "/node_modules/pdfjs-dist/build/pdf.worker.mjs";

    const canvas = document.querySelector("#pdf-page");
    const context = canvas.getContext("2d");
    const error = document.querySelector("#error");

    try {
      const pdf = await pdfjsLib.getDocument("/files/example.pdf").promise;
      const page = await pdf.getPage(1);
      const viewport = page.getViewport({ scale: 1.5 });
      const pixelRatio = window.devicePixelRatio || 1;

      // Backing pixels use the device ratio for a sharp image; CSS dimensions
      // keep the displayed page at the intended size.
      canvas.width = Math.floor(viewport.width * pixelRatio);
      canvas.height = Math.floor(viewport.height * pixelRatio);
      canvas.style.width = `${Math.floor(viewport.width)}px`;
      canvas.style.height = `${Math.floor(viewport.height)}px`;

      await page.render({
        canvas,
        canvasContext: context,
        viewport,
        transform: pixelRatio === 1
          ? null
          : [pixelRatio, 0, 0, pixelRatio, 0, 0],
      }).promise;
    } catch (cause) {
      console.error(cause);
      error.textContent = `Could not load the PDF: ${cause.message}`;
    }
  </script>
</body>
</html>

The sequence is document → page → viewport → canvas sizing → render. getDocument() and getPage() return promises, so failures must be handled with try/catch or promise rejection handlers. The device-pixel-ratio transform improves sharpness on high-density screens while the CSS size remains readable.

Run the example correctly

  1. Install pdfjs-dist and confirm the installed version.
  2. Put a reachable PDF at /files/example.pdf, or change the URL in getDocument().
  3. Start an HTTP server from the project directory, for example npx http-server ..
  4. Open the server URL, such as http://localhost:8080/, in your browser.
  5. Use Developer Tools’ Network and Console tabs if the page or worker fails.

Do not double-click the HTML file. PDF.js workers are not enabled for file:// pages, and browser security rules also make local file requests behave differently from deployed HTTP(S) requests.

Library and worker setup

Keep versions identical

The display module and worker are a pair. If your bundler imports PDF.js 6.3.289 but serves a worker from another release, document loading can fail with version or worker errors. Import both files from the same installed package and release, or pin the same version when using a CDN. Check the release shown in Mozilla’s setup documentation before copying a prebuilt URL.

Bundlers and copied assets

Bundlers often rewrite module URLs or emit worker files into a build directory. The required result is the same: the browser must be able to fetch pdf.mjs and the matching pdf.worker.mjs. If your tool cannot serve a worker module directly, copy the worker to a public assets directory and assign its public URL to GlobalWorkerOptions.workerSrc.

Prebuilt viewer or display API?

Choice Best for Trade-off
Display API A custom canvas, page layout, or application-specific controls You must build navigation, zoom, search, accessibility behavior, and other controls
Prebuilt viewer A broader PDF interface that you can adapt You must customize and maintain the viewer with each PDF.js release

Mozilla describes the viewer as a starting point and asks embedders to re-skin it or build upon it rather than ship an unmodified copy. A custom display-layer integration gives more control; the viewer supplies more UI functionality immediately.

Loading PDFs from another origin

A URL passed to getDocument() is fetched under normal browser same-origin rules. A PDF on another domain must return CORS headers allowing your page’s origin. Otherwise the browser blocks the request before PDF.js can render it. The simplest production arrangement is to serve the PDF from the same origin as the application.

const loadingTask = pdfjsLib.getDocument({
  url: "https://cdn.example.com/manual.pdf",
  // Other documented loading options can be supplied here when needed.
});
const pdf = await loadingTask.promise;

Inspect the PDF response in Network tools. Confirm the status code, content type, redirects, and Access-Control-Allow-Origin response. CORS must be configured by the PDF host; adding a request header in browser JavaScript cannot bypass the browser’s policy.

Render other pages and add navigation

Use pdf.numPages to expose page controls and call pdf.getPage(pageNumber) for the selected page. Cancel or serialize renders when users click quickly so an older render does not overwrite a newer one.

let pdf;
let renderTask;
let pageNumber = 1;

async function loadDocument(url) {
  pdf = await pdfjsLib.getDocument(url).promise;
  pageNumber = 1;
  await renderPage(pageNumber);
}

async function renderPage(number) {
  if (renderTask) renderTask.cancel();
  const page = await pdf.getPage(number);
  const viewport = page.getViewport({ scale: 1.25 });
  const ratio = window.devicePixelRatio || 1;
  canvas.width = Math.floor(viewport.width * ratio);
  canvas.height = Math.floor(viewport.height * ratio);
  canvas.style.width = `${Math.floor(viewport.width)}px`;
  canvas.style.height = `${Math.floor(viewport.height)}px`;
  renderTask = page.render({
    canvas,
    canvasContext: context,
    viewport,
    transform: ratio === 1 ? null : [ratio, 0, 0, ratio, 0, 0],
  });
  await renderTask.promise;
}

For a continuous document view, create one canvas per visible page and render pages as they approach the viewport. Rendering every page at once increases memory use, especially for large PDFs or high device-pixel ratios.

Useful rendering options

  • Scale: choose a viewport scale for the displayed size; increase it for zoom and reduce it for thumbnails.
  • Device pixel ratio: size the canvas backing store by window.devicePixelRatio and set CSS dimensions separately for sharp output.
  • Page selection: PDF page numbers start at 1; validate user input against pdf.numPages.
  • Responsive layouts: recompute the viewport and render again when the container width changes.
  • Accessibility: provide a meaningful canvas label and consider a text layer or an alternate document link when users need selectable or assistive-technology-readable text.

Troubleshooting

Symptom Likely cause Fix
Worker was not enabled The page was opened with file:// Serve it through HTTP(S), even during local development.
Setting up fake worker or worker version mismatch Worker URL is missing, inaccessible, or from another PDF.js release Set GlobalWorkerOptions.workerSrc to the worker shipped with the same package version and verify its Network response.
Failed to fetch or CORS error PDF is on another origin without permission Serve the file from the app origin or configure the PDF host’s CORS policy.
404 for the PDF The URL is relative to a different base path than expected Open the PDF URL directly, fix the path, and account for your deployment’s base URL.
Blank canvas Rendering was not awaited, canvas dimensions are zero, or a prior render replaced it Await page.render(...).promise, set non-zero dimensions, and serialize or cancel concurrent renders.
Blurry output Canvas backing pixels equal CSS pixels on a high-density display Multiply backing dimensions by devicePixelRatio and pass the matching transform.
Large memory use Many high-resolution pages rendered simultaneously Render visible pages on demand, lower scale for thumbnails, and release old canvases.
PDF opens in the browser but PDF.js rejects it The response may be an HTML error page, redirect, or unsupported/malformed content Inspect the response body and headers, follow the final URL, and verify that the fetched resource is the intended PDF.

Performance and reliability checklist

  • Serve PDF.js assets and PDFs over HTTPS in production.
  • Pin and update the library and worker together.
  • Cache immutable PDF assets with appropriate HTTP cache headers.
  • Render only visible pages for long documents.
  • Use a lower scale for previews and increase it for an explicit zoom action.
  • Show loading and error states because network fetches and parsing are asynchronous.
  • Log the failing URL, HTTP status, worker URL, and PDF.js version when diagnosing production failures.
  • Test same-origin and cross-origin deployments separately; a development setup can hide a production CORS problem.

Or skip the browser setup

If your goal is a rendered PDF or image rather than an interactive in-browser viewer, ScreenshotNeo provides a website screenshot API and PDF capture endpoint. It accepts one GET request and can handle the browser setup on your behalf. Read the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing result. An MCP server lets Claude, Cursor, and other MCP clients call screenshot and PDF tools. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Can I use PDF.js without the full viewer?

Yes. The display API is enough for a custom canvas integration. The full viewer is optional and provides a broader interface to adapt.

Why must the worker match the library?

The display code and worker are released together. A mismatched worker can produce setup or version errors, so serve both from the same installed release.

Does PDF.js upload the PDF to a server?

In this browser flow, the page requests the PDF URL through Fetch or XHR and parses it in the browser. The URL still has to satisfy normal same-origin or CORS rules.

How do I make a multi-page viewer?

Read pdf.numPages, create a page control, call getPage(number), and render each selected or visible page. Lazy rendering keeps long documents responsive.

When should I choose a hosted capture API?

Use one when you need a rendered PDF or screenshot without maintaining browser workers, CORS handling, page controls, and rendering code. For that workflow, the ScreenshotNeo request above is the shortest path.