EmbedPDF

Quick start

This guide builds a small PDF viewer: the pages, text people can select, zoom buttons, a highlighter and a save button. Each step adds one plugin or one control, so you can stop at any step and have a working viewer.

1. Install#

You need two packages: the one for your framework, and an engine.

npm install @embedpdf/angular@next @embedpdf/engine@next

Every feature is a separate import from the framework package, such as @embedpdf/angular/stage, so what you don’t use never reaches your bundle.

2. Show a PDF#

Create the engine once, then give it to provideEmbedPdf() with the plugins you want and the document to open. <epdf-stage> shows the pages, and <epdf-render-layer> draws each one:

Loading live preview…

basic.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { localEngine } from '@embedpdf/engine';

// The local engine opens bytes: fetch lazily, under the loading tab.
const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// The component that provides the viewer renders it: the engine is created in the browser,
// and costs nothing until the first document opens, so the page renders at once.
@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div style="height: 500px">
      <!-- Document UI is defined over a document: gate it on having one. -->
      <epdf-stage *epdfDocumentGate="let document; fallback: loading" style="height: 100%">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </div>

    <ng-template #loading><p>Loading…</p></ng-template>
  `,
})
export class App {}

localEngine() costs nothing until it’s used, so it’s safe in your component, even with server-side rendering. The viewer starts it in the browser: PDFium boots in a Web Worker in the background, and the first page shows as soon as the document is open. The engine’s WebAssembly is part of your build and loads from your own site; see Setup.

*epdfDocumentGate shows its fallback until the document is open, so the Stage never renders without one.

3. Let people select text#

Add the interaction plugin, which handles the pointer for every feature, and the selection plugin. Then put <epdf-selection-layer> above the page:

select-text.ts
import { Component } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf, type OpenInput } from '@embedpdf/angular/runtime';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

@Component({
  selector: 'app-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfSelectionLayer],
  providers: [
    provideEmbedPdf(
      {
        engine: () => localEngine(),
        initialDocuments: [{ source: ebook }],
      },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
    ),
  ],
  template: `
    <epdf-stage *epdfDocumentGate style="height: 500px">
      <ng-template epdfPage>
        <epdf-render-layer />
        <epdf-selection-layer />
      </ng-template>
    </epdf-stage>
  `,
})
export class App {}

Drag over text to select it, double-click a word, or triple-click a line. On a phone, long-press a word. See Selecting & copying for copying and a selection menu.

4. Add zoom buttons#

Every plugin has a service with its methods and its state as signals, and the Stage has EpdfStageRef. Next to the Stage in a template, its template reference, #stage="epdfStage", gives you both:

zoom-controls.ts
import { PercentPipe } from '@angular/common';
import { Component } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf, type OpenInput } from '@embedpdf/angular/runtime';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

@Component({
  selector: 'app-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    PercentPipe,
  ],
  providers: [
    provideEmbedPdf(
      {
        engine: () => localEngine(),
        initialDocuments: [{ source: ebook }],
      },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
    ),
  ],
  template: `
    <section *epdfDocumentGate>
      <div>
        <button (click)="stage.zoomOut()">−</button>
        <span>{{ stage.zoomLevel() | percent }}</span>
        <button (click)="stage.zoomIn()">+</button>
      </div>

      <epdf-stage #stage="epdfStage" style="height: 500px">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-selection-layer />
        </ng-template>
      </epdf-stage>
    </section>
  `,
})
export class App {}

The buttons go inside *epdfDocumentGate, next to the stage they name. The percentage updates when the zoom changes, whether a button, a pinch or code changed it.

5. Add a highlighter#

The annotation plugin shows the annotations a PDF already has, and lets people add their own. Add it with its layer, and a button that turns on the highlight tool:

highlighter.ts
import { Component, computed, inject } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf, type OpenInput } from '@embedpdf/angular/runtime';
import { EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfInteraction, withInteraction } from '@embedpdf/angular/interaction';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

@Component({
  selector: 'app-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    EpdfAnnotationLayer,
  ],
  providers: [
    provideEmbedPdf(
      {
        engine: () => localEngine(),
        initialDocuments: [{ source: ebook }],
      },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
      withAnnotation(),
    ),
  ],
  template: `
    <button [attr.aria-pressed]="highlighting()" (click)="toggleHighlight()">Highlight</button>

    <epdf-stage *epdfDocumentGate style="height: 500px">
      <ng-template epdfPage>
        <epdf-render-layer />
        <epdf-selection-layer />
        <epdf-annotation-layer />
      </ng-template>
    </epdf-stage>
  `,
})
export class App {
  private readonly interaction = inject(EpdfInteraction);
  protected readonly highlighting = computed(() => this.interaction.activeToolId() === 'highlight');

  protected toggleHighlight() {
    this.interaction.activateTool(this.highlighting() ? 'pointer' : 'highlight');
  }
}

With the tool on, select text and it’s highlighted. Click a highlight with the pointer tool to select it. The page picture leaves the annotations out by itself, because the annotation layer draws them now. The component that provides the viewer can inject its services too, so the button needs no component of its own. See Annotations for every tool.

6. Save#

documents.download() gives you the PDF with every change, as bytes. saveFile hands them to the browser as a download:

save-button.ts
import { Component, inject } from '@angular/core';
import { EpdfDocuments, saveFile } from '@embedpdf/angular/runtime';

@Component({
  selector: 'app-save-button',
  template: `<button (click)="save()">Save</button>`,
})
export class SaveButton {
  private readonly documents = inject(EpdfDocuments);

  protected async save() {
    saveFile(await this.documents.download(), 'reviewed.pdf');
  }
}
Put <app-save-button /> anywhere in the template of the component that provides the viewer: it injects the service from there.

Next steps#

Was this page helpful?

Your feedback goes directly to the documentation team.