Opening documents
Give the viewer the documents to open when it starts, and *epdfDocumentGate shows your content once
the document is ready:
import { Component } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { engine } from './pdf';
import { Spinner } from './spinner';
@Component({
selector: 'app-contract-viewer',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, Spinner],
providers: [
provideEmbedPdf(
{ engine, initialDocuments: [{ source: { kind: 'url', url: '/files/contract.pdf' } }] },
withStage(),
withRender(),
),
],
template: `
<epdf-stage *epdfDocumentGate="let document; fallback: opening">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
<ng-template #opening><app-spinner /></ng-template>
`,
})
export class ContractViewer {}The viewer is on screen at once; the document opens in the background. Components outside
*epdfDocumentGate, such as your toolbar’s frame, render right away.
Open a document later#
inject(EpdfDocuments) opens documents while the app runs, for example one the user picked:
import { Component, inject } from '@angular/core';
import { EpdfDocuments } from '@embedpdf/angular/runtime';
@Component({
selector: 'app-open-button',
template: `<input #picker type="file" accept="application/pdf" (change)="open(picker.files![0])" />`,
})
export class OpenButton {
private readonly documents = inject(EpdfDocuments);
protected async open(file: File) {
await this.documents.open({ kind: 'bytes', bytes: await file.arrayBuffer() }, { name: file.name });
}
}open() resolves with { document } once it’s open, and the new document becomes the active one.
Pass { activate: false } to open it in the background. Pick a PDF of your own here:
Loading live preview…
import { ChangeDetectionStrategy, Component, inject, ViewEncapsulation } from '@angular/core';
import {
EpdfDocument,
EpdfDocumentGate,
EpdfDocuments,
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';
const welcome = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/welcome.pdf');
return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: welcome, name: 'welcome.pdf' }] },
withStage(),
withRender(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './open.css',
encapsulation: ViewEncapsulation.None,
template: `
<div class="toolbar">
<span class="document">
@if (document.id()) {
<strong>{{ document.name() ?? 'Untitled' }}</strong>
{{
document.status() === 'ready'
? ' · ' + document.pageCount() + ' pages'
: ' · ' + document.status()
}}
} @else {
No document open
}
</span>
<label class="button picker">
Open a PDF…
<input #picker type="file" accept="application/pdf" (change)="pick(picker)" />
</label>
@if (document.id()) {
<button type="button" class="button" (click)="documents.close(document.id())">Close</button>
} @else {
<button type="button" class="button" (click)="documents.open(welcome, { name: 'welcome.pdf' })">
Open the welcome again
</button>
}
</div>
<epdf-stage *epdfDocumentGate="let ready; fallback: empty" class="stage">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
<!-- The gate's fallback: a document on its way, or none at all. -->
<ng-template #empty>
<p class="loading">
{{ document.id() ? 'Opening…' : 'No document is open. Open a PDF to see it here.' }}
</p>
</ng-template>
`,
})
export class App {
protected readonly documents = inject(EpdfDocuments);
protected readonly document = inject(EpdfDocument);
protected readonly welcome = welcome;
// A file the user picks opens next to the welcome, and becomes the active document.
protected async pick(picker: HTMLInputElement) {
const file = picker.files?.[0];
picker.value = '';
if (!file) return;
await this.documents.open(
{ kind: 'bytes', bytes: await file.arrayBuffer() },
{ name: file.name },
);
}
}When an input or a route parameter names the document, give it to the Stage instead:
<epdf-stage [document]="source()"> opens the document, and when the signal changes, it opens the
new one and closes the old one.
import { Component, computed, input } from '@angular/core';
import { EpdfRenderLayer } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage } from '@embedpdf/angular/stage';
@Component({
selector: 'app-contract-page',
imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
template: `
<epdf-stage [document]="source()">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
`,
})
export class ContractPage {
/** `/contracts/:id`, bound with `withComponentInputBinding()` */
readonly id = input.required<string>();
protected readonly source = computed(() => ({ kind: 'url', url: `/files/${this.id()}.pdf` }) as const);
}Where a document comes from#
| Source | What it is |
|---|---|
{ kind: 'url', url } | A file the viewer fetches. The URL can be relative to your site. |
{ kind: 'bytes', bytes } | A file you already have: an upload, a file picker, a download. |
async (signal) => source | Anything else: a function that returns one of the above. |
The viewer calls a function under the document’s loading state, and signal fires if the user
closes the document first, so pass it on to what the function waits for.
Files that need a sign-in#
When your server wants a header or a cookie for a file, give the viewer your own fetch. Every
{ kind: 'url' } document downloads through it: it gets the URL, and an init with the
document’s cancel signal to pass on.
import { Component } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { getToken } from './auth';
import { engine } from './pdf';
@Component({
selector: 'app-contract-viewer',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
providers: [
provideEmbedPdf(
{
engine,
initialDocuments: [{ source: { kind: 'url', url: '/api/files/contract.pdf' } }],
fetch: (url, init) =>
fetch(url, { ...init, headers: { Authorization: `Bearer ${getToken()}` } }),
},
withStage(),
withRender(),
),
],
template: `
<epdf-stage *epdfDocumentGate="let document">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
`,
})
export class ContractViewer {}For your cookies on another site, add credentials: 'include' instead; that site’s CORS headers
have to allow it. A server that answers 401 or 403 makes the document’s error
permission-denied, and documents.retry(id) calls your fetch again, so a token it reads when
it’s called is a fresh one. The engine’s own files never go through it.
Loading, errors and passwords#
*epdfDocumentGate has something to show for each state a document can be in:
import { Component } from '@angular/core';
import { EpdfDocumentGate } from '@embedpdf/angular/runtime';
import { EpdfRenderLayer } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage } from '@embedpdf/angular/stage';
import { PasswordForm } from './password-form';
import { Spinner } from './spinner';
@Component({
selector: 'app-document-view',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, PasswordForm, Spinner],
template: `
<epdf-stage *epdfDocumentGate="let document; fallback: opening; locked: locked; error: failed">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
<ng-template #opening><app-spinner /></ng-template>
<ng-template #locked let-document><app-password-form [document]="document" /></ng-template>
<ng-template #failed let-document>
<p>Couldn't open {{ document.name }}: {{ document.error.message }}</p>
</ng-template>
`,
})
export class DocumentView {}A password-protected PDF opens locked. Ask for the password and unlock it:
import { Component, inject, input, signal } from '@angular/core';
import { EpdfDocuments, type DocumentInfo } from '@embedpdf/angular/runtime';
import { PasswordPrompt } from './password-prompt';
@Component({
selector: 'app-password-form',
imports: [PasswordPrompt],
template: `<app-password-prompt [wrong]="wrong()" (submitted)="submit($event)" />`,
})
export class PasswordForm {
readonly document = input.required<DocumentInfo>();
private readonly documents = inject(EpdfDocuments);
protected readonly wrong = signal(false);
protected async submit(password: string) {
try {
await this.documents.unlock(this.document().id, { password });
} catch {
this.wrong.set(true); // it stays locked; ask again
}
}
}After an error, such as a network failure, documents.retry(id) tries again. Here the second
document isn’t a PDF, so it shows its error:
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
inject,
input,
linkedSignal,
signal,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, EpdfDocuments, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { DocumentInfo, FailedDocumentInfo, 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';
const welcome = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/welcome.pdf');
return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// A file that isn't a PDF, so its document can't be opened.
const broken: OpenInput = { kind: 'bytes', bytes: new TextEncoder().encode('Not a PDF') };
@Component({
selector: 'demo-password-form',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="panel">
<h3>{{ document().name }} needs a password</h3>
@if (wrong()) {
<p>That password isn’t right. Try again.</p>
}
<div class="actions">
<input
#field
class="field"
type="password"
aria-label="Password"
[value]="password()"
(input)="password.set(field.value)"
/>
<button type="button" class="button" (click)="unlock()">Unlock</button>
</div>
</div>
`,
})
export class PasswordForm {
readonly document = input.required<DocumentInfo>();
private readonly documents = inject(EpdfDocuments);
protected readonly password = signal('');
protected readonly wrong = linkedSignal(() => this.document().passwordProvided ?? false);
protected async unlock() {
try {
await this.documents.unlock(this.document().id, { password: this.password() });
} catch {
this.wrong.set(true); // it stays locked; ask again
}
}
}
@Component({
selector: 'demo-open-error',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="panel" data-tone="error">
<h3>Couldn’t open {{ document().name }}</h3>
<p>{{ document().error.message }}</p>
<div class="actions">
<button type="button" class="button" (click)="documents.retry(document().id)">
Try again
</button>
<button type="button" class="button" (click)="documents.close(document().id)">Close</button>
</div>
</div>
`,
})
export class OpenError {
readonly document = input.required<FailedDocumentInfo>();
protected readonly documents = inject(EpdfDocuments);
}
@Component({
selector: 'demo-root',
imports: [
EpdfDocumentGate,
EpdfStage,
EpdfPageTemplate,
EpdfRenderLayer,
PasswordForm,
OpenError,
],
providers: [
provideEmbedPdf(
{
engine: () => localEngine(),
initialDocuments: [
{ source: welcome, name: 'welcome.pdf' },
{ source: broken, name: 'broken.pdf', active: true },
],
},
withStage(),
withRender(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './states.css',
encapsulation: ViewEncapsulation.None,
template: `
<div class="tabs" role="tablist">
@for (document of documents.documents(); track document.id) {
<button
type="button"
role="tab"
class="tab"
[attr.aria-selected]="document.id === documents.activeId()"
(click)="documents.setActive(document.id)"
>
<span class="status" [attr.data-status]="document.status"></span>
{{ document.name }}
</button>
}
</div>
<epdf-stage
*epdfDocumentGate="let ready; fallback: opening; locked: locked; error: failed"
class="stage"
>
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
<ng-template #opening><div class="panel">Opening…</div></ng-template>
<ng-template #locked let-document>
<demo-password-form [document]="document" />
</ng-template>
<ng-template #failed let-document>
<demo-open-error [document]="document" />
</ng-template>
`,
})
export class App {
protected readonly documents = inject(EpdfDocuments);
}To show the state yourself, for example on a tab, inject(EpdfDocument) gives you the document’s
status: 'loading', 'locked', 'ready' or 'error'. Every field is listed under
State.
Once a document is 'ready', each page’s picture can still take a moment to arrive. To show that
too, see Show that a page is loading.
Who the user is and what they may do#
Tell the viewer who the user is and what they may do, once. Every document it opens uses them:
import { Component } from '@angular/core';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { engine } from './pdf';
@Component({
selector: 'app-review-viewer',
imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
providers: [
provideEmbedPdf(
{
engine,
identity: { userId: 'u_381', displayName: 'Dana Smith' },
scope: ['doc.open', 'doc.render', 'doc.text.select', 'doc.annotate.read', 'annotations:create:self'],
},
withStage(),
withRender(),
),
],
template: `
<epdf-stage>
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
`,
})
export class ReviewViewer {}identityis who the user is. TheirdisplayNamebecomes the author of the annotations they make.scopeis what they may do, as a list of permissions. Leave it out and they may do what the file allows: everything, unless the PDF restricts itself, as in Acrobat.
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
effect,
inject,
signal,
ViewEncapsulation,
} from '@angular/core';
import {
EpdfDocument,
EpdfDocumentGate,
EpdfDocuments,
EpdfViewer,
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';
const welcome = async (): Promise<OpenInput> => {
const response = await fetch('/pdfs/welcome.pdf');
return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};
const dana = { userId: 'u_381', displayName: 'Dana Smith' };
// What each role may do, as permissions.
const roles = {
reader: ['doc.open', 'doc.render', 'doc.text.select'],
editor: [
'doc.open',
'doc.render',
'doc.text.select',
'doc.text.copy',
'doc.download',
'doc.print',
],
};
type Role = keyof typeof roles;
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
providers: [
provideEmbedPdf(
{
engine: () => localEngine(),
identity: dana,
scope: roles.reader,
initialDocuments: [{ source: welcome, name: 'welcome.pdf' }],
},
withStage(),
withRender(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './identity.css',
encapsulation: ViewEncapsulation.None,
template: `
<div class="toolbar">
<label class="label">
Dana Smith, as
<select #picker class="select" [value]="role()" (change)="role.set(asRole(picker.value))">
<option value="reader">reader</option>
<option value="editor">editor</option>
</select>
</label>
<!-- Read the document's id too, so the checks are read again when it opens or closes. -->
@let id = document.id();
<span class="check" [attr.data-allowed]="!!id && documents.canDownload()"> Download </span>
<span class="check" [attr.data-allowed]="!!id && documents.canPrint()">Print</span>
</div>
<p class="note">
This document opened for a {{ openedAs() }}.
@if (openedAs() !== role()) {
<button type="button" class="button" (click)="openAgain()">
Open it again as {{ role() }}
</button>
}
</p>
<epdf-stage *epdfDocumentGate="let ready; fallback: opening" class="stage">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
<ng-template #opening><p class="loading">Opening…</p></ng-template>
`,
})
export class App {
protected readonly documents = inject(EpdfDocuments);
protected readonly document = inject(EpdfDocument);
protected readonly role = signal<Role>('reader');
// The role the open document was opened with: it keeps that one.
protected readonly openedAs = signal<Role>('reader');
constructor() {
// The viewer's scope follows the role; documents opened from now on get it.
const viewer = inject(EpdfViewer);
effect(() => viewer.updateSettings({ scope: roles[this.role()] }));
}
protected asRole(value: string): Role {
return value as Role;
}
protected async openAgain() {
const role = this.role();
await this.documents.close(this.document.id());
await this.documents.open(welcome, { name: 'welcome.pdf' });
this.openedAs.set(role);
}
}inject(EpdfViewer).updateSettings({ identity, scope }), for example when
someone else signs in. The documents you open after that use the new values; documents already
open keep theirs.
A document can have its own, which replace the viewer’s for that document:
const initialDocuments = [{ source, scope: ['doc.open', 'doc.render'] }]; // this one is read-only
await documents.open(source, { scope: ['doc.open', 'doc.render'] }); // and so is this onePermissions lists every permission.
Close a document#
await documents.close(document.id);Closing frees the document’s memory in the engine. The next document becomes the active one.
Methods#
What inject(EpdfDocuments) gives you for opening and closing documents and reading their pages. Tabs and
the active document are on Several documents, downloading and printing on
Saving & downloading.
| Method | What it does |
|---|---|
open(source, options?) | Opens a document and makes it the active one. Resolves { document } once it’s ready, or locked with a password; rejects when it can’t be opened. Open a document later |
canOpenFiles() | Whether open() takes a file’s bytes, such as a file the user picked, or a URL it downloads: true on the local engine, false on CloudPDF, where every document comes from your server |
openAll(documents) | Opens several documents, as initialDocuments does: every tab appears at once, in order, and the one marked active (or the first) becomes the active one. A failure shows in that document’s status instead of throwing. |
unlock(id, { password }) | Unlocks a locked document. Rejects on a wrong password, and the document stays locked so you can ask again. Loading, errors and passwords |
retry(id, options?) | Tries a failed document again, with the same source and options. Resolves like open(). A document from a grant takes what it waited for: { passphrase } or { identity }. Loading, errors and passwords |
identify(id, answers) | Answers what the document’s grant asks before the first change (its identityRequest): the change waiting goes out. Rejects while a required answer is missing or one can’t be taken, and the change keeps waiting. |
declineIdentity(id) | Takes back the change waiting for the visitor’s answers; the next change asks again. |
close(id), closeAll() | Closes one document, or every document, and frees its memory. Close a document |
get(id?) | A document’s fields, the ones inject(EpdfDocument) gives you, or null when it isn’t open. Without an id, the document in scope |
listPages(id?) | The document’s pages, in order. In a component, use document.pages(). |
getPage(page, id?) | One page, by its ref or its index, or null when the document doesn’t have it |
getPageIndex(ref, id?) | A page’s position, from 0, or -1 when the document doesn’t have it |
getRevision(id?) | A number that goes up each time pages are added, removed, moved or rotated; -1 with no document |
options for open() are name (what its tab shows), activate: false (open it in the
background), password, identity (who your page knows the user is), and, on the local engine,
scope. Without an id, the page
methods use the document in scope: the one a [epdfDocumentScope]
around your component names, or else the active one. To stop an open that’s still loading, close the
document: that cancels its download too.
State#
inject(EpdfDocument) gives you the document a component talks to: the active one, or the one its
[epdfDocumentScope] names. Each field is a signal: read it in a template or a computed(), and your view updates when it changes. Outside a component,
get(id) gives the same fields. With no document open yet, it reads as one still on its way: an
empty id and status: 'loading', so a component that shows it never has to check for null.
| Field | What it is |
|---|---|
id() | The document’s id, for close(id), unlock(id, …) and [epdfDocumentScope] |
name() | The name its tab shows, from open() or rename() |
status() | 'loading', 'locked', 'ready' or 'error' |
pageCount() | How many pages it has; 0 until it’s ready |
hasUnsavedChanges() | Whether it has changes that weren’t downloaded yet. Warn before changes are lost |
passwordProvided() | When it’s locked: whether a password was tried and was wrong |
error() | Why it couldn’t be opened, when status is 'error': { code, message } |
grantRequest() | When it’s from a grant: the passphrase or the questions it waits for, to open or to go on |
identityRequest() | When it’s ready and a change waits for the visitor’s answers to the grant’s questions |
Every open document, with the same fields, comes from inject(EpdfDocuments).
Events#
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
import { EpdfDocuments } from '@embedpdf/angular/runtime';
@Component({
selector: 'app-open-error',
template: `<p role="alert">{{ message() }}</p>`,
})
export class OpenError {
private readonly documents = inject(EpdfDocuments);
protected readonly message = toSignal(
this.documents.openFailed$.pipe(map(({ error }) => `Couldn't open the file: ${error.message}`)),
{ initialValue: '' },
);
}| Event | Fires when | Carries |
|---|---|---|
opened$ | A document is open and ready | documentId, info |
openFailed$ | A document couldn’t be opened | documentId, error |
locked$ | A document needs a password | documentId, passwordProvided |
closed$ | A document was closed | documentId |
pagesChanged$ | Pages were added, removed, moved or rotated | documentId, pages, revision |
Switching the active document is on Several documents.
Good to know#
Opening large files#
The local engine needs the whole file before it can show the first page, so a large PDF on a slow connection takes as long as its download.
initialDocuments is read once#
Like engine, provideEmbedPdf() reads initialDocuments once, when the viewer starts. To open
another document later, use documents.open(), or give the Stage a [document] that changes.
Several documents#
A viewer can hold several documents at once, like tabs. See Several documents.
Your feedback goes directly to the documentation team.