---
title: "Setup — Angular"
description: "Install the headless packages for your framework, create the engine once, and use them with server-side rendering."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/setup"
---

# Setup

A headless viewer needs two packages: the one for your framework, and an engine. This page
installs them, creates the engine for your whole app, and covers server-side rendering, bundlers
and fonts.

## Install

```sh
pnpm add @embedpdf/angular@next @embedpdf/engine@next
```

The framework package holds every plugin, one import path each (`@embedpdf/angular/search`,
`@embedpdf/angular/annotation`). A plugin you don't import isn't in your bundle.

It needs Angular 20.2 or later. Everything is standalone, `OnPush` and built on signals, so it works
in zoneless apps (`provideZonelessChangeDetection()`), the default for new ones.

## Create the engine once

Create one engine when your app starts, and share it with every viewer:

```ts
// pdf.ts
import { localEngine } from '@embedpdf/engine';

export const engine = localEngine();
```

Creating the engine is instant and allocates nothing. The first viewer that uses it starts it, and
PDFium boots in a Web Worker in the background.

`provideEmbedPdf()` can also own the engine. Pass a function: the viewer calls it in the browser
when it starts, and destroys the engine when the component or route that provides it goes away:

**`owned-engine.ts`**

```typescript
import { Component } from '@angular/core';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { withRender } from '@embedpdf/angular/render';
import { withStage } from '@embedpdf/angular/stage';

@Component({
  selector: 'app-document-viewer',
  providers: [
    provideEmbedPdf(
      { engine: () => import('@embedpdf/engine').then((m) => m.localEngine()) },
      withStage(),
      withRender(),
    ),
  ],
  template: `<!-- your toolbar and pages -->`,
})
export class DocumentViewer {}
```

The function may return a promise. With `import()`, as here, the engine loads in its own chunk the
first time a viewer needs it, instead of with your page.

An engine you pass in yourself is never destroyed by the viewer, so one engine can serve every
viewer in your app.

## Register the plugins

Each plugin is a feature of `provideEmbedPdf()`, given with the engine. Put it in the `providers`
of the component that shows the viewer:

**`plugins.ts`**

```typescript
import { Component } from '@angular/core';
import { provideEmbedPdf } 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 { engine } from './pdf';

@Component({
  selector: 'app-document-viewer',
  imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfSelectionLayer],
  providers: [
    provideEmbedPdf({ engine }, withStage(), withRender(), withInteraction(), withSelection()),
  ],
  template: `
    <epdf-stage>
      <ng-template epdfPage>
        <epdf-render-layer />
        <epdf-selection-layer />
      </ng-template>
    </epdf-stage>
  `,
})
export class DocumentViewer {}
```

That component and everything in its template share one viewer, and the component can `inject()`
the plugins' services itself. In `app.config.ts` or a route's `providers`, the same call gives the
whole app or route one viewer, so a toolbar in your header and the pages in your outlet work
together. The viewer reads the engine and the features once, when it starts.

A setup that can't work, such as a service with no viewer above it, throws an error with a
number and its fix, like Angular's own. [Errors](errors) lists them.

Order doesn't matter, and a plugin that needs another says so when the app starts: the annotation
plugin, for example, needs the interaction plugin.

## Server-side rendering

The headless packages render nothing on the server that needs the engine: the engine only starts in
the browser. So a module-level engine is safe everywhere.

With Angular SSR, the viewer works as it is, and the server renders the page around it. To keep its
code, the engine included, out of the first download, put it in a `@defer (on viewport)` block: it
loads when it scrolls into view.

**`ssr.ts`**

```typescript
import { Component } from '@angular/core';
import { DocumentViewer } from './document-viewer';

@Component({
  selector: 'app-contract-page',
  imports: [DocumentViewer],
  template: `
    @defer (on viewport) {
      <app-document-viewer />
    } @placeholder {
      <div style="height: 600px">Loading the viewer…</div>
    }
  `,
})
export class ContractPage {}
```

The server renders the `@placeholder`, and the deferred component loads in its own chunk with
everything it imports. `on viewport` watches the placeholder, so give it the viewer's height, and
nothing moves when the viewer takes its place.

## Bundlers and self-hosting

The engine's worker and WebAssembly ship inside the package, and your build picks them up.

The Angular CLI needs no configuration. Its application builder can't copy the WebAssembly as a
file, so `@embedpdf/engine` gives it its portable build without being asked: the WebAssembly
travels inside your build as a lazy chunk and is unpacked in the browser, a moment's work when the
engine starts.

There's nothing to copy, and nothing is fetched from a CDN. The engine loads from your own site, so
it works offline and behind a firewall.

If your setup can't bundle a worker, for example with a strict Content Security Policy, host the
files yourself: [Self-hosting](https://www.embedpdf.com/docs/viewer/angular/self-hosting) lists every option, with notes for Parcel
and plain esbuild.

## Fonts

Text the engine draws, such as a text box or a filled form field, needs a font with its characters.
For languages the built-in fonts don't cover, give the engine a fallback font:

```ts
export const engine = localEngine({
  fallbackFonts: [{ key: 'noto-cjk', familyName: 'Noto Sans CJK', url: '/fonts/NotoSansCJK.ttf' }],
});
```

[Fonts](https://www.embedpdf.com/docs/engine/documents/fonts) covers registering fonts and using them by name. Every other
engine option is in the engine's [Setup](https://www.embedpdf.com/docs/engine/setup#options).

> **The same viewer, on your server**
>
> Move a headless viewer to CloudPDF by changing one line: `localEngine()` becomes
> `cloudEngine()`. Your plugins, components and services stay as they are.
> Files stay on your server, the first page shows without downloading the PDF, and fonts, limits and
> storage are the server's to configure.
>
> [Set up CloudPDF](https://www.cloudpdf.com/docs/headless/angular/setup)

## Good to know

### One viewer, many documents

A viewer can hold several open documents at once, like tabs. See
[Several documents](documents/tabs).

### Changing `engine` or `plugins`

Both are read once, when the viewer starts. To use another engine or other plugins, create the
component that provides them again, for example under another route or an `@if`.

> The ready-made [Viewer](https://www.embedpdf.com/docs/viewer/angular) uses these same packages. If you start there, everything on
> these pages still applies when you move to headless.
