Skip to content

PDF Viewer ​

Angular standalone component for rendering PDF documents with pdf.js. Drop-in replacement for ng2-pdf-viewer updated for Angular 21+, the signals API, zoneless change detection, and pdfjs-dist 5.

Live Demo ​

Try the component in your browser: Open Demo →

Installation ​

bash
npm install @dasch-ng/pdf-viewer

or with bun:

bash
bun add @dasch-ng/pdf-viewer

Features ​

  • 📦 Standalone component - No NgModule, just import where needed
  • 🎯 Signals API - All inputs/outputs as Angular signals (Angular 21+)
  • 🪶 Zoneless ready - Works with provideZonelessChangeDetection()
  • 🔍 Drop-in compatible - Matches the ng2-pdf-viewer template API
  • 🎨 Configurable - Page, zoom, rotation, text rendering, link targets, fit-to-page
  • 📡 Events - Load progress, page render, page change, errors
  • 🌐 CDN worker - PDF worker auto-loaded from jsDelivr (overridable)

Basic Usage ​

typescript
import { Component } from '@angular/core';
import { PdfViewerComponent } from '@dasch-ng/pdf-viewer';

@Component({
  selector: 'app-pdf',
  imports: [PdfViewerComponent],
  template: ` <pdf-viewer [src]="'/assets/sample.pdf'" [render-text]="true" [original-size]="false" style="width: 100%; height: 600px;" /> `,
})
export class PdfComponent {}

Migration from ng2-pdf-viewer ​

Only the package name changes. Template API (selector, attributes, events) is preserved.

diff
- import { PdfViewerComponent } from 'ng2-pdf-viewer';
+ import { PdfViewerComponent } from '@dasch-ng/pdf-viewer';

API Reference ​

See the generated API reference for the complete list of inputs, outputs, and types.

Inputs (selected) ​

InputTypeDefaultDescription
srcstring | Uint8Array | PDFSource—PDF source (URL, byte array, or pdf.js source object)
pagenumber1Current page number (1-based)
zoomnumber1Zoom factor (must be > 0)
zoom-scale'page-width' | 'page-fit' | 'page-height''page-width'Zoom scaling mode
rotationnumber0Rotation in degrees (multiples of 90)
original-sizebooleantrueRender at original size, no auto-fit
show-allbooleantrueShow all pages vs. single page
stick-to-pagebooleanfalseSnap navigation to page boundaries
render-textbooleantrueRender selectable text layer
render-text-modeRenderTextModeENABLEDText rendering mode
external-link-target'blank' | 'none' | 'self' | 'parent' | 'top''blank'Target for external links
autoresizebooleantrueRe-render on container resize
fit-to-pagebooleanfalseFit page to viewport
show-bordersbooleanfalseShow page borders
c-maps-urlstringunpkg CDNCMaps URL for non-Latin fonts

Outputs ​

OutputPayloadDescription
after-load-completePDFDocumentProxyFired when document finishes loading
pages-initializedCustomEventPages initialized in the viewer
page-renderedCustomEventSingle page rendered
text-layer-renderedCustomEventText layer rendered
errorunknownLoad or render error
on-progressPDFProgressDataDownload progress updates
pageChangenumberCurrent page changed

Worker setup ​

pdf.js runs the actual PDF parsing in a Web Worker. The worker needs to be reachable at runtime. The library does not bundle the worker file: pdfjs-dist ships it as part of its npm package, and your application decides how to serve it.

Default: jsDelivr CDN (zero config) ​

If you do nothing, PdfViewerComponent configures GlobalWorkerOptions.workerSrc to:

text
https://cdn.jsdelivr.net/npm/pdfjs-dist@<version>/legacy/build/pdf.worker.min.mjs

This works out of the box and is convenient for prototypes, but means each visitor's browser issues a third-party request to jsDelivr. For production apps that care about GDPR, offline support, or stable version pinning, self-host the worker instead.

Self-host the worker (recommended for production) ​

Step 1: copy the worker file into your application build via your Angular app's angular.json assets array:

json
{
  "projects": {
    "your-app": {
      "architect": {
        "build": {
          "options": {
            "assets": [
              {
                "glob": "pdf.worker.min.mjs",
                "input": "node_modules/pdfjs-dist/legacy/build",
                "output": "assets/pdfjs"
              }
            ]
          }
        }
      }
    }
  }
}

(If you're on the new @angular/build:application builder, the assets array has the same shape.)

Step 2: point the component at that URL before the first PdfViewerComponent is instantiated. The simplest place is main.ts, right before bootstrapApplication(...):

typescript
// main.ts
(window as any).pdfWorkerSrc = new URL('assets/pdfjs/pdf.worker.min.mjs', document.baseURI).href;

bootstrapApplication(AppComponent, { providers: [...] });

Or in index.html before Angular boots:

html
<script>
  window.pdfWorkerSrc = new URL('assets/pdfjs/pdf.worker.min.mjs', document.baseURI).href;
</script>

Now the worker is served from your own origin, from the pdfjs-dist version in your node_modules.

The worker and the pdfjs-dist code bundled into your app must be the exact same version, otherwise pdf.js refuses to load the document. If your app declares a different pdfjs-dist version, set the asset input to node_modules/@dasch-ng/pdf-viewer/node_modules/pdfjs-dist/legacy/build.

Other CDN requests

Self-hosting the worker does not remove every third-party request. By default, CMaps and annotation icons still load from unpkg. Set the c-maps-url input to a self-hosted copy of pdfjs-dist/cmaps/. The annotation icon path cannot be changed yet.

Version-pinned override ​

If you need a worker URL that only applies to one pdf.js version (rare), use the version-keyed property. Replace <version> with the installed pdfjs-dist version:

typescript
(window as any)['pdfWorkerSrc<version>'] = '/assets/pdfjs/pdf.worker-<version>.min.mjs';

The component reads pdfWorkerSrc<currentPdfJsVersion> first, then falls back to plain pdfWorkerSrc, then to the CDN.

Why isn't the worker shipped inside @dasch-ng/pdf-viewer? ​

pdfjs-dist is already declared as a dependency of @dasch-ng/pdf-viewer, so the worker file is always present in your node_modules tree. Re-shipping it inside our package would store the same file twice. The asset-copy approach above lets you choose the deployment path. The version itself is set by @dasch-ng/pdf-viewer, which pins an exact pdfjs-dist version.

Examples ​

Render a PDF from a remote URL ​

typescript
import { Component, signal } from '@angular/core';
import { PdfViewerComponent } from '@dasch-ng/pdf-viewer';

@Component({
  selector: 'app-remote-pdf',
  imports: [PdfViewerComponent],
  template: ` <pdf-viewer [src]="pdfSrc()" [render-text]="true" [original-size]="false" style="display: block; width: 100%; height: 600px;" /> `,
})
export class RemotePdfComponent {
  pdfSrc = signal('https://dasch.ng/demos/pdf-viewer/assets/pdf-test.pdf');
}

Render a local file picked by the user ​

typescript
import { Component, signal } from '@angular/core';
import { PdfViewerComponent, PDFSource } from '@dasch-ng/pdf-viewer';

@Component({
  selector: 'app-local-pdf',
  imports: [PdfViewerComponent],
  template: `
    <input type="file" accept="application/pdf" (change)="onFile($event)" />
    @if (pdfSrc()) {
      <pdf-viewer [src]="pdfSrc()!" style="display:block; width:100%; height:600px;" />
    }
  `,
})
export class LocalPdfComponent {
  pdfSrc = signal<string | Uint8Array | PDFSource | null>(null);

  onFile(event: Event) {
    const file = (event.target as HTMLInputElement).files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = (e) => this.pdfSrc.set(e.target?.result as ArrayBuffer);
    reader.readAsArrayBuffer(file);
  }
}

React to load completion and progress ​

typescript
import { Component, signal } from '@angular/core';
import { PdfViewerComponent, PDFDocumentProxy, PDFProgressData } from '@dasch-ng/pdf-viewer';

@Component({
  selector: 'app-pdf-events',
  imports: [PdfViewerComponent],
  template: `
    <pdf-viewer
      [src]="'/assets/sample.pdf'"
      (after-load-complete)="onLoaded($event)"
      (on-progress)="onProgress($event)"
      style="display:block; width:100%; height:600px;"
    />
    <p>Pages: {{ numPages() ?? '…' }} — Loaded: {{ progress() }}%</p>
  `,
})
export class PdfEventsComponent {
  numPages = signal<number | null>(null);
  progress = signal(0);

  onLoaded(pdf: PDFDocumentProxy) {
    this.numPages.set(pdf.numPages);
  }

  onProgress(data: PDFProgressData) {
    if (data.total) this.progress.set(Math.round((data.loaded / data.total) * 100));
  }
}

Bind page navigation to a signal ​

typescript
import { Component, signal } from '@angular/core';
import { PdfViewerComponent } from '@dasch-ng/pdf-viewer';

@Component({
  selector: 'app-pdf-paginated',
  imports: [PdfViewerComponent],
  template: `
    <button (click)="page.set(page() - 1)">Prev</button>
    <button (click)="page.set(page() + 1)">Next</button>
    <pdf-viewer [src]="'/assets/sample.pdf'" [page]="page()" [show-all]="false" style="display:block; width:100%; height:600px;" />
  `,
})
export class PdfPaginatedComponent {
  page = signal(1);
}

A working playground combining all of the above is available at the live demo.

Credits ​

This library is a fork of ng2-pdf-viewer by Vadym Yatsyuk (@VadimDez) and its many contributors.

The Angular 21 / pdfjs-dist 5 / signals upgrade was contributed by Luqman-Ud-Din Muhammad in VadimDez/ng2-pdf-viewer#1228.

Full upstream git history was preserved when importing this library into the monorepo.

License ​

MIT

Released under the MIT License.