npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nalashaacontrols/pdf-viewer

v0.1.6

Published

Framework-independent, production-grade PDF viewer library for the web (TypeScript, zero runtime dependencies).

Readme

@nalashaa/pdf-viewer

A production-grade, framework-independent PDF viewer library for the web — strict TypeScript, zero runtime dependencies, with its own PDF engine (parser → interpreter → renderer). Rendering, virtualization, text selection, search, annotations, AcroForm forms (fill and design/save), visual signatures, printing, and accessibility — usable from plain JavaScript, React, or Vue.

PDF Viewer — toolbar, sidebar icon rail with thumbnails, and a rendered page

Status: pre-1.0 (early access). The API below is implemented and tested, but may still change before 1.0 (changes are recorded in CHANGELOG.md). Text currently renders through approximated system fonts (standard-14 metrics keep positioning faithful); glyph-exact embedded-font rendering is on the roadmap. Pages using embedded fonts report an embedded-font diagnostic via viewer.getDiagnostics().


Features

  • 📄 Own PDF engine — lexer, parser, xref (classic tables, xref streams, object streams), filters, page tree; built from ISO 32000, not wrapped around another viewer. Images include JPEG (DCTDecode) and JPEG 2000 (JPXDecode) via the engine's own decoder — scanned/flattened documents whose whole page is one JPX image render correctly.
  • 🔐 Encrypted PDFs — standard security handler: RC4 (40/128-bit), AES-128, AES-256; password prompt flow with typed error codes.
  • 🖼️ Virtualized rendering — only visible pages hold live canvases; smooth scrolling and zoom on large documents, off-main-thread parsing via a worker (with main-thread fallback).
  • 🔎 Text selection, copy, and search — a find-in-document dropdown (toolbar magnifier / Ctrl+F) with live match highlighting, next/previous stepping, match counter, and Match case / Match any word options; the same search is available as a typed API.
  • 🗂️ Organize Pages — full-viewer overlay to reorder (drag & drop), rotate, duplicate, delete, and import pages from another PDF, with undo/redo; Save applies the new sequence, Save As downloads a copy.
  • ⚡ Lazy loading for huge files — load(url, { lazyLoading: true }) opens documents over HTTP Range requests: a 1 GB PDF shows its first page in ~2 s after transferring only a few MB.
  • ✏️ Annotations — highlight, underline, strikethrough, squiggly, line, arrow, rectangle, circle, polygon, free text, freehand ink, sticky note; style editing (stroke/fill color, stroke width, opacity), undo/redo, JSON export/import.
  • 📝 Forms (AcroForm) — read and fill text fields, checkboxes, radios, dropdowns, listboxes; signature fields render as a click-to-sign "Sign" box with an auto-dated companion date box; form designer (addField/removeField) and save() that writes values, new fields, and applied signatures back into the PDF bytes as an incremental update.
  • 🖋️ Visual signatures — draw, type, or upload a signature image and place it on the page; signatures applied to signature fields are embedded into the saved bytes so any PDF reader displays them (visual marks only, no cryptographic signing).
  • 🖨️ Printing — per-page rasterization with page ranges and DPI control.
  • 🧭 Navigation — thumbnails, outline/bookmarks, page links, history (back/forward), destinations.
  • 🎨 Toolbar + sidebar UI — a top toolbar plus an always-visible left icon rail (thumbnails, annotations, outline, Organize Pages) with a flyout panel; fully optional — every action is also a command and a typed API call, so you can build your own chrome.
  • ♿ Accessibility — keyboard shortcuts, ARIA wiring, focus management.
  • 🧩 React and Vue adapters included (optional peer dependencies), plus a <pdfv-viewer> custom element for Angular and any other framework.
  • 🔒 Security-first — PDF bytes are treated as untrusted input; embedded JavaScript is never executed; external links only open through an explicit host callback/opt-in.

Installation

npm install @nalashaa/pdf-viewer

The package ships ESM bundles, TypeScript declarations, a CSS file, and an IIFE bundle for script-tag use. No runtime dependencies. React/Vue are optional peer dependencies used only by the adapter entry points.

Workers and bundlers

Parsing runs off the main thread in a PDF worker that ships inside the package (dist/assets/pdf-worker-*.js) and is resolved relative to the package's own files — no copying, no workerSrc-style configuration. This works out of the box with Vite, webpack 5, and plain <script type="module">. If your dev server ever reports a 404 for the worker (some setups pre-bundle dependencies in a way that breaks import.meta.url resolution), exclude the package from pre-bundling — for Vite:

// vite.config.ts of your app
export default defineConfig({
  optimizeDeps: { exclude: ["@nalashaa/pdf-viewer"] },
});

If the worker cannot start at all, the viewer degrades gracefully to main-thread parsing and reports a worker-unavailable diagnostic.

Quick start

import { PdfViewer } from "@nalashaa/pdf-viewer";
import "@nalashaa/pdf-viewer/styles";

const viewer = new PdfViewer({
  container: "#viewer", // CSS selector or HTMLElement (required)
  toolbar: true, // built-in toolbar (default: true)
  sidebar: true, // icon rail: thumbnails / annotations / outline / organize (default: true)
  theme: "light", // "light" | "dark"
});

await viewer.load("/documents/sample.pdf");

Give the container an explicit size — the viewer fills it:

<div id="viewer" style="width: 100%; height: 100vh;"></div>

Script tag (no bundler)

The IIFE bundle exposes a PdfViewer global:

<link rel="stylesheet" href="pdf-viewer.css" />
<script src="pdf-viewer.min.js"></script>
<script>
  const viewer = new PdfViewer({ container: "#viewer" });
  viewer.load("sample.pdf");
</script>

Loading documents

load() accepts a URL string, URL, ArrayBuffer, Uint8Array, Blob/ File, or an existing PdfDocument:

await viewer.load("/docs/report.pdf");
await viewer.load(new Uint8Array(bytes));
await viewer.load(fileInput.files[0]);

Huge PDFs — lazy loading. For URL sources, { lazyLoading: true } opens the document over HTTP Range requests instead of downloading the whole file first — a 1 GB document shows its first page in a couple of seconds after transferring only a few MB, and further pages fetch their own byte ranges as you scroll or jump:

await viewer.load("/docs/huge-scan.pdf", { lazyLoading: true });

The server must honor Range headers (most static file servers and CDNs do); when it doesn't, the viewer automatically falls back to a normal full download. Limitations: lazily-loaded documents parse on the main thread and cannot be saved (Save / Organize Pages disable) — reload normally when you need to edit.

In the UI: the toolbar's first button, Open PDF file (command openFile), shows the browser's file picker and loads the chosen PDF — no host code needed. Next to it, New form template (command newFormTemplate) creates a blank single-page document and opens the form designer row, ready to place fields (see Designing fields); download the finished template with the Save PDF button. The same blank document is available programmatically:

import { createBlankPdf } from "@nalashaa/pdf-viewer";

await viewer.load(createBlankPdf()); // one empty US Letter page
await viewer.load(createBlankPdf({ pageCount: 2, pageSize: { width: 595.28, height: 841.89 } })); // A4

Encrypted (password-protected) PDFs

Documents with an empty user password open transparently. Otherwise pass a password and handle the typed error codes to prompt and retry:

import { PdfParseError } from "@nalashaa/pdf-viewer";

try {
  await viewer.load("/docs/secure.pdf", { password: userInput });
} catch (error) {
  if (error instanceof PdfParseError) {
    if (error.code === "PDF_PASSWORD_REQUIRED") {
      // no password was given — ask the user
    } else if (error.code === "PDF_PASSWORD_INCORRECT") {
      // wrong password — ask again
    }
  }
}

Unloading and cleanup

viewer.unload(); // close the document, keep the viewer
viewer.destroy(); // dispose the viewer and release the container

Options

All options except container are optional.

| Option | Type | Default | Description | | -------------------------- | --------------------------- | ------------ | ----------------------------------------------------------------------------------------------------- | | container | string \| HTMLElement | — (required) | Element (or selector) the viewer mounts into. | | toolbar | boolean \| ToolbarOptions | true | Built-in toolbar; pass a ToolbarOptions object to hide/append items. | | sidebar | boolean | true | Always-visible icon rail with thumbnails, annotations, outline, and Organize Pages. | | theme | "light" \| "dark" | "light" | Color theme (change later with setTheme). | | locale | string | "en" | BCP-47 locale for UI strings (see Localization). | | onExternalLink | (url: string) => boolean | — | Called when the user activates an external link; return true to allow opening in a new tab. | | openExternalLinks | boolean | false | Allow safe-scheme links to open without a callback. The viewer never navigates on its own by default. | | features.annotations | boolean | true | Annotation tools, layer, panel, and API. | | features.forms | boolean | true | Form widgets and API. | | features.signatures | boolean | true | Visual signature workflows. | | features.organizePages | boolean | true | Organize Pages overlay and its icon-rail button. | | signatures.persist | boolean | false | Store signatures in device-local storage for reuse (opt-in). | | signatures.maxImageBytes | number | 2 MiB | Byte cap for uploaded signature images. | | signatures.fonts | SignatureFontDef[] | built-in | Font choices for typed signatures (system font stacks). |

Custom toolbar buttons

toolbar.extraItems adds your own buttons with your own icon (an inner-SVG string drawn on a 20×20 viewBox). Give each item an onClick handler — it is auto-registered as command toolbar.custom.<id> — or a command you registered yourself via viewer.commands.register() (useful when you need enable/disable logic through canExecute). placement positions the button: { before: "<itemId>" } / { after: "<itemId>" } anchors it next to any default item id (see DEFAULT_TOOLBAR_ITEMS), and { index: 2 } puts it at a fixed 0-based slot in the header's item order (out-of-range indexes clamp to the ends). Omit it to append at the toolbar's right edge. Default ids also work in hiddenItems.

const viewer = new PdfViewer({
  container: "#viewer",
  toolbar: {
    hiddenItems: ["openFile", "print"],
    extraItems: [
      {
        type: "button",
        id: "stampApproved",
        label: "Stamp as approved",
        icon: `<rect x="3" y="5" width="14" height="10" rx="2"
                 fill="none" stroke="currentColor" stroke-width="1.6"/>
               <path d="M6.5 10l2.5 2.5 4.5-5" fill="none"
                 stroke="currentColor" stroke-width="1.6"/>`,
        onClick: () => myApp.stampCurrentPage(),
        placement: { before: "saveDocument" },
      },
    ],
  },
});

// Custom clicks also fire an event (handy with the React/Vue `events` prop):
viewer.on("toolbarItemClicked", ({ itemId, command }) => {
  console.log(`custom toolbar button ${itemId} clicked (${command})`);
});

A live demo button ships in examples/basic (npm run dev) — click the speech-bubble icon next to Print and watch the event log.

API overview

Everything the toolbar does is also available programmatically. The main surface (see plan/27-public-api.md for the full contract):

Navigation & zoom

viewer.getPageCount();
viewer.getCurrentPage(); // 1-based
viewer.goToPage(5);
viewer.nextPage();
viewer.previousPage();
viewer.firstPage();
viewer.lastPage();
viewer.goBack();
viewer.goForward(); // navigation history

viewer.getZoom(); // e.g. 1.25
viewer.setZoom(2); // number or "fitWidth" | "fitPage"
viewer.zoomIn();
viewer.zoomOut();
viewer.fitWidth();
viewer.fitPage();

viewer.getRotation(); // 0 | 90 | 180 | 270
viewer.rotateClockwise();
viewer.rotateCounterClockwise();

Search

In the UI: the toolbar's magnifier button (command toggleSearch, or Ctrl/Cmd+F while the viewer has focus) opens a find-in-document dropdown: type to search live, Enter / Shift+Enter step through matches, the counter shows how many were found, and Match case / Match any word toggle the matching mode. Esc closes it and clears the highlights.

Programmatically:

const session = viewer.search("diagnosis", { caseSensitive: false });
session.totalMatches; // number of hits
session.next(); // move to next match
session.previous();
session.close(); // clear highlights

// Match each word independently instead of the exact phrase:
viewer.search("asthma insulin", { anyWord: true });

viewer.findNext();
viewer.findPrevious();

Print & fullscreen

await viewer.print(); // whole document
await viewer.print({ range: "1-5, 8, 11-" }); // page ranges
await viewer.print({ dpi: 300 });
viewer.toggleFullscreen();

Events

All events are typed (PdfViewerEventMap). on/off are aliases of addEventListener/removeEventListener; on returns an unsubscribe function:

const off = viewer.on("documentLoaded", (e) => console.log(e.pageCount));
viewer.on("pageChanged", (e) => console.log("page", e.pageNumber));
viewer.on("zoomChanged", (e) => console.log("zoom", e.zoom));
viewer.on("annotationAdded", (e) => console.log(e.annotation));
viewer.on("formFieldChanged", (e) => console.log(e.fqn, e.value));
viewer.on("linkOpenRequested", (e) => window.open(e.url, "_blank"));
viewer.on("error", (e) => console.error(e.error));
off();

Common events: documentLoading, documentLoaded, documentLoadFailed, pageChanged, pageRendered, zoomChanged, rotationChanged, searchStarted/searchProgress/searchCompleted, toolChanged, annotationAdded/annotationUpdated/annotationDeleted, formFieldChanged, signatureAdded, printStarted/printProgress/ printCompleted, fullscreenChanged, linkOpenRequested, error.

Commands

Toolbar buttons, keyboard shortcuts, and the API all execute the same named commands — useful for building custom UI:

await viewer.commands.execute("zoomIn");
await viewer.commands.execute("annotate.highlight");
viewer.commands.isEnabled("print");

Annotations

Arm a tool (the user then draws/selects on the page), or create annotations programmatically:

// Tools: "highlight" | "underline" | "strikeout" | "squiggly" | "line"
// | "arrow" | "rectangle" | "circle" | "polygon" | "ink" | "freeText"
// | "note" | "signature" | "none"
viewer.annotations.setTool("highlight");

// Programmatic creation
const rect = viewer.annotations.add({
  type: "rectangle",
  pageIndex: 0,
  bounds: { x: 72, y: 500, width: 200, height: 80 },
  strokeWidth: 2,
});

// Styling — patches the selection (undoable) and sets future defaults
viewer.annotations.select(rect.id);
viewer.annotations.setStyle({
  color: { r: 220, g: 60, b: 50 }, // stroke color
  fillColor: { r: 255, g: 240, b: 200 }, // null clears the fill
  strokeWidth: 4,
  opacity: 0.8,
});

viewer.annotations.undo();
viewer.annotations.redo();
viewer.annotations.update(rect.id, { locked: true });
viewer.annotations.delete(rect.id);

// Persistence (JSON round-trip)
const data = viewer.annotations.export();
viewer.annotations.import(data);

In the built-in UI, the Edit annotations toolbar button opens a second row with the text-markup tools, a Shapes dropdown (line, arrow, rectangle, circle, polygon), a Measure dropdown, free text, freehand ink, note, and the style controls (stroke color, fill color, stroke width, opacity) plus delete.

Free text (typewriter)

Free text is styled like text, not like a shape. Arm the Free text tool (or annotations.setTool("freeText")) and click the page; the inline editor opens in the annotation's own styling, and a contextual control group appears in the annotation toolbar — font family, size, color, bold, italic, underline, strikethrough and alignment — shown only while free text is armed or selected. color is the text color; fillColor and borderColor style the box behind it.

viewer.annotations.setTool("freeText");

// Or style the selection / the next-created free text:
viewer.annotations.setStyle({
  fontFamily: "Times New Roman", // base-14 only — see below
  fontSize: 18,
  bold: true,
  italic: false,
  underline: true,
  strikethrough: false,
  textAlign: "center",
  color: { r: 10, g: 20, b: 30 },        // TEXT color
  fillColor: { r: 255, g: 250, b: 205 }, // background box; null clears
  borderColor: { r: 90, g: 90, b: 90 },  // null removes the border
});

Fonts are limited to the base-14 set — "Helvetica", "Courier", "Times New Roman", "Symbol", "ZapfDingbats" — because the library embeds no fonts, and any other family would be silently substituted by the reader. Every PDF reader has these 14, so a saved file looks the same everywhere. Symbol and ZapfDingbats have a single face each, so bold and italic disable for them. The same list styles designed form fields, via the exported BASE14_FONT_FAMILIES / base14BaseFont() helpers.

Saved PDFs carry all of it: the chosen face is written as the annotation's /DA font and staged into its appearance stream, alignment as /Q, the box as painted fill and border, and underline/strikethrough as drawn rules (PDF text state has no way to declare either).

Limitation: centred and right-aligned placement uses an average-advance width estimate rather than real font metrics (the package ships no base-14 metric tables), so long centred lines can sit a few points off. Courier is exact; left-aligned text is unaffected.

Measurement

The Measure dropdown holds distance (drag a line), perimeter (click vertices, double-click to finish), and area (click vertices to close a polygon) — each renders its value as a label that updates when you zoom, move, or recalibrate. Calibrate sets the scale: draw a line over something with a known size and enter its real length in the dialog; every measurement re-labels. Default scale is the PDF's own 72 pt = 1 in.

viewer.annotations.setTool("distance"); // or "perimeter" | "area" | "calibrate"
viewer.annotations.setMeasurementScale({ unit: "ft", pointsPerUnit: 14.4 }); // 72pt = 5ft
viewer.annotations.getMeasurementScale(); // { unit: "ft", pointsPerUnit: 14.4 }

Forms (AcroForm)

Reading and filling

viewer.forms.getFields(); // readonly FormField[]
viewer.forms.getValue("patient.name");
viewer.forms.setValue("patient.name", "Jane Doe");
viewer.forms.setValues({ "consent.signed": true, "visit.type": "follow-up" });
viewer.forms.isSigned("physicianSignature"); // signature field signed this session?
viewer.forms.reset(); // restore defaults

Designing fields

In the UI: the toolbar's Design form fields button opens a second row with a hand (move) tool and one tool per field type (text, checkbox, radio group, dropdown, list box, signature). Pick a tool and click on the page — the field is placed at its proper fixed size, with a dashed preview following your cursor. The radio tool stays armed so each tap adds another option to the same group. The signature tool places a fixed-size signature box plus a companion date box beneath it in one step: the box renders with a "Sign" badge, clicking it opens the signature dialog (draw, type, or upload), the result is fitted into the box, and the date box auto-fills with the signing date. To reposition anything you placed, switch to the hand tool (or just finish placing — non-radio tools return to it automatically): click a field to select it, drag to move, pull the corner handle to resize, and edit its properties right in the toolbar row — label/tooltip, required, read-only, and the option list for dropdowns and list boxes — or delete it with the trash button. Everything is undoable and saved by forms.save() like any other field — or one click on the toolbar's Save PDF button, which downloads the document with all values and designed fields written in. The same tools are available as commands (formField.text, formField.checkbox, …, toggleFormDesignerToolbar, saveFormDesign) and announce state via the formDesignerToolChanged event. The designer row's right-aligned Save button (saveFormDesign) finishes editing: the row closes and the form switches back to fill mode.

Programmatically:

const field = viewer.forms.addField({
  type: "text", // "text" | "checkbox" | "radio" | "combo" | "listbox" | "signature"
  name: "followUpNotes", // partial name (/T) — no periods, must be unique
  pageIndex: 0,
  rect: { x: 72, y: 200, width: 300, height: 24 },
  multiline: true,
});
viewer.forms.removeField(field.fqn);

A signature field is placed the same way (spec type NewSignatureFormField). It renders as a bordered box with a "Sign" badge; clicking it opens the signature dialog. If you also want the auto-filled date, add a text field named <signatureName>Date — the viewer fills it with the signing date (MM/DD/YYYY) when the signature is applied (the UI tool creates this pair for you):

viewer.forms.addField({
  type: "signature",
  name: "physicianSignature",
  pageIndex: 0,
  rect: { x: 72, y: 120, width: 200, height: 60 },
});
viewer.forms.addField({
  type: "text",
  name: "physicianSignatureDate", // "<signatureName>Date" → auto-filled on sign
  label: "Date signed",
  pageIndex: 0,
  rect: { x: 72, y: 86, width: 200, height: 24 },
});

Exporting and importing form data

Snapshot the filled values as portable JSON (e.g. to store drafts in your backend) and re-apply them later — on the same document or a fresh load:

const data = viewer.forms.exportData(); // { schema: "pdfv-form-data@1", values: {...} }
// …persist anywhere, then later:
const report = viewer.forms.importData(data);
report.rejected; // unknown fields / bad values, valid ones still apply

Saving back to PDF

save() returns the complete document bytes with current field values, any designer-added fields, and any signatures applied to signature fields written as an incremental update (the original bytes are preserved; a new revision is appended):

const bytes = await viewer.forms.save();
const blob = new Blob([bytes], { type: "application/pdf" });
// download, upload, or reload it

Applied signatures are embedded as locked stamp appearances fitted to the field's box — drawn signatures as exact vector strokes, typed signatures as italic text, uploaded images as embedded image data — so Acrobat, browsers, and other readers display the mark. This is a visual mark, not a cryptographic signature; reopening the saved file in this viewer shows the field itself as re-signable (importing the mark back into signing state is on the roadmap).

The toolbar's Save PDF button (command saveDocument) does this in one click and downloads the result; the Print button drives the same pipeline as viewer.print().

Organize Pages

The Organize Pages button on the left icon rail (command organizePages) opens a full-viewer overlay showing every page as a selectable thumbnail:

  • Select All or click individual tiles, then rotate left/right, duplicate, or delete the selection (a document always keeps at least one page). Undo/redo is available inside the overlay.
  • Drag to reorder: drag a tile to a new position (dragging a selected tile moves the whole selection as a block); an accent bar shows where the pages will land. Ctrl/Cmd+←/→ moves the focused tile by keyboard.
  • Import Document appends every page of another PDF (deep-copied into the saved file).
  • Save applies the new page sequence and reloads the viewer with it — pending form values, designer-added fields, and applied signatures are written in first, so nothing is lost. Save As downloads a copy and leaves the open document untouched.
  • Both fire the pagesOrganized event: { pageCount, mode: "save" | "saveAs" }.
// Disable the feature (hides the toolbar button, disables the command):
new PdfViewer({ container: "#viewer", features: { organizePages: false } });

// Open programmatically:
await viewer.commands.execute("organizePages");
viewer.on("pagesOrganized", (e) => console.log(e.mode, e.pageCount));

Current limitations: encrypted PDFs are refused (same as form saving); duplicated and imported pages are written without their annotations/form widgets; saved files grow (incremental updates never remove old objects).

Signatures

Visual signatures only — the library draws a signature mark; it does not perform cryptographic (digital) signing.

There are two ways to sign:

  • Signature form fields — any signature field (parsed from the PDF or placed with the form designer) renders as a "Sign" box. The user clicks it, the signature dialog opens, and the accepted signature is displayed inside the box; a companion <name>Date text field auto-fills with the signing date. Clicking a signed box again replaces the signature. Check the state with viewer.forms.isSigned(fqn); viewer.forms.save() embeds the mark into the PDF bytes (see Saving back to PDF and Designing fields).
  • Free placement — activate the signature tool and click anywhere on the page:
viewer.annotations.setTool("signature"); // or the toolbar button

The dialog offers draw / type / upload modes. Signatures are never stored unless the host opts in with signatures: { persist: true }.

React

import { PdfViewerComponent } from "@nalashaa/pdf-viewer/react";
import "@nalashaa/pdf-viewer/styles";

function Report() {
  return (
    <PdfViewerComponent
      src="/docs/chart.pdf"
      style={{ height: "100vh" }}
      viewerOptions={{ theme: "dark" }}
      events={{ pageChanged: (e) => console.log(e.pageNumber) }}
      onReady={(viewer) => {
        /* imperative access */
      }}
      onLoadError={(error) => {
        /* e.g. prompt for password */
      }}
    />
  );
}

The forwarded ref resolves to the underlying PdfViewer instance.

For a complete e-sign container — load a base64 PDF from Redux, validate that the form is filled or signed, and export the signed bytes as base64 (a drop-in migration from @syncfusion/ej2-react-pdfviewer) — see examples/react-sign/pdf-sign-container.jsx.

Vue

<script setup lang="ts">
import { PdfViewerComponent } from "@nalashaa/pdf-viewer/vue";
import "@nalashaa/pdf-viewer/styles";
</script>

<template>
  <PdfViewerComponent
    src="/docs/chart.pdf"
    :viewer-options="{ theme: 'dark' }"
    :events="{ pageChanged: (e) => console.log(e.pageNumber) }"
    style="height: 100vh"
    @ready="
      (viewer) => {
        /* imperative access */
      }
    "
    @load-error="
      (error) => {
        /* e.g. prompt for password */
      }
    "
  />
</template>

Angular (and any other framework)

Angular consumes the viewer through the custom element entry — no Angular-specific build needed:

// main.ts
import { definePdfViewerElement } from "@nalashaa/pdf-viewer/element";
import "@nalashaa/pdf-viewer/styles";
definePdfViewerElement(); // registers <pdfv-viewer>
@Component({
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <pdfv-viewer
      [src]="pdfBytes"
      style="display: block; height: 100vh"
      (documentLoaded)="onLoaded($event)"
      (load-error)="onError($event)"
    ></pdfv-viewer>
  `,
})
export class ReportComponent { /* payloads arrive in $event.detail */ }

src takes a URL attribute or bytes/Blob via property binding; every viewer event re-dispatches as a same-named DOM CustomEvent; the element's viewer property exposes the full imperative API (forms, annotations, commands, …). The same element works in plain HTML, Svelte, or anywhere else. See examples/angular/README.md for a complete guide, including a typed Angular wrapper component.

Localization

English ships built-in. Register additional locales and switch at runtime:

import { registerLocale, setLocale } from "@nalashaa/pdf-viewer";

registerLocale("de", {/* LocaleStrings */});
setLocale("de");
// or per viewer: new PdfViewer({ container, locale: "de" })

Theming

The UI is styled through --pdfv-* CSS custom properties and pdfv- prefixed classes, with built-in light and dark themes:

viewer.setTheme("dark");

Override the custom properties on your container to match your brand.

Security

PDF bytes are treated as untrusted input throughout the engine:

  • JavaScript embedded in PDFs is never executed.
  • External links open only through your onExternalLink callback or the openExternalLinks opt-in; javascript:, file:, data: and unknown schemes are always blocked.
  • Launch actions, attachments, and embedded files are inert by default.
  • Parser boundaries validate lengths, counts, recursion depth, and allocation sizes (decompression bombs, cyclic references).

Malformed or unsupported constructs fail with typed PdfParseError / UnsupportedFeatureError values and viewer.getDiagnostics() reports — never silently corrupted output.

Browser support

Evergreen Chrome, Edge, Firefox, and Safari (last two major versions), desktop and mobile. Baseline: ES2020, IntersectionObserver, ResizeObserver. Optional capabilities (OffscreenCanvas, module workers, createImageBitmap) are feature-detected with graceful fallbacks.

Development

npm install
npm run dev        # example app (examples/basic)
npm run check      # typecheck + lint + tests
npm run test:e2e   # Playwright browser tests
npm run build      # dist/: ESM + IIFE bundles, types, CSS

Test documents are self-generated (never third-party samples):

node scripts/make-sample-pdf.mjs        # small example PDF (examples/basic)
node scripts/make-big-pdf.mjs 1024      # ~1 GB / ~85k-page text PDF for
                                        # stress testing (gitignored)

The example app's Load 1GB PDF button loads the generated src/assets/big-text.pdf to exercise virtualization and the canvas memory budget under a huge document.

License

Licensed under the MIT License. Built as an independent implementation from the ISO 32000 specification; contains no code from commercial PDF viewers.