Links
Most PDFs have links: a table of contents that jumps to a chapter, a footnote to a website. Register
withLink() and put <epdf-link-layer> in each page, and they work:
import { Component } from '@angular/core';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfLinkLayer, withLink } from '@embedpdf/angular/link';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { engine } from './pdf';
@Component({
selector: 'app-reader',
imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfLinkLayer],
providers: [provideEmbedPdf({ engine }, withStage(), withRender(), withInteraction(), withLink())],
template: `
<epdf-stage>
<ng-template epdfPage>
<epdf-render-layer />
<epdf-link-layer />
</ng-template>
</epdf-stage>
`,
})
export class Reader {}Loading live preview…
import {
ChangeDetectionStrategy,
Component,
effect,
inject,
untracked,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotation, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfLinkLayer, withLink } from '@embedpdf/angular/link';
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()) };
};
// Where the two links go on the first page, in page coordinates.
const TO_PAGE_3 = { x: 72, y: 24, width: 160, height: 28 };
const TO_WEBSITE = { x: 250, y: 24, width: 190, height: 28 };
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfLinkLayer],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(),
withRender(),
withInteraction(),
// The annotation plugin is only here to make the links below.
withAnnotation(),
withLink(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './basic.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<p class="hint">
Two links at the top of the first page. Click one, or press Tab to reach it and Enter to
follow it.
</p>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-link-layer />
</ng-template>
</epdf-stage>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
private readonly annotation = inject(EpdfAnnotation);
private readonly document = inject(EpdfDocument);
private added = false;
constructor() {
// Made on load, since the document has none: a label the page shows, and a link over it.
effect(() => {
const [first, , third] = this.document.pages();
if (!first || !third || this.added) return;
this.added = true;
untracked(() => void this.addLinks(first.ref, third.ref));
});
}
private async addLinks(first: PageRef, third: PageRef) {
const label = (text: string, box: typeof TO_PAGE_3) =>
this.annotation.create(first, {
subtype: 'free-text',
box,
contents: text,
fontSize: 13,
fontColor: '#054fb3',
interiorColor: '#e8f1ff',
color: '#7db6ff',
strokeWidth: 1,
});
await label('Go to page 3 →', TO_PAGE_3);
await label('Open embedpdf.com ↗', TO_WEBSITE);
await this.annotation.create(first, {
subtype: 'link',
rect: TO_PAGE_3,
target: { kind: 'goto', destination: { kind: 'fit', page: third } },
});
await this.annotation.create(first, {
subtype: 'link',
rect: TO_WEBSITE,
target: { kind: 'uri', uri: 'https://www.embedpdf.com' },
});
}
}- A link to a page goes there, to the exact spot and zoom the link names.
- A link to a website opens in a new tab, after a safety check (see Good to know).
- A link with a PDF action, such as a button that resets a form, runs the action when the actions plugin is registered.
The pointer turns into a hand over a link, and a link can be reached with the Tab key and followed with Enter.
Change how links look#
By default a link area is invisible, as in other PDF apps. <epdf-link-layer><ng-template epdfLink> lets you draw it
yourself, for example with an underline on hover:
import { NgTemplateOutlet } from '@angular/common';
import { Component } from '@angular/core';
import { EpdfLinkLayer, EpdfLinkTemplate } from '@embedpdf/angular/link';
@Component({
selector: 'app-links',
imports: [EpdfLinkLayer, EpdfLinkTemplate, NgTemplateOutlet],
template: `
<epdf-link-layer>
<ng-template epdfLink let-link let-native="native">
<span class="pdf-link" [attr.title]="link.target.kind === 'uri' ? link.target.uri : null">
<ng-container [ngTemplateOutlet]="native" />
</span>
</ng-template>
</epdf-link-layer>
`,
})
export class Links {}Loading live preview…
import { NgTemplateOutlet } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
effect,
inject,
signal,
untracked,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotation, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfLinkLayer, EpdfLinkTemplate, withLink } from '@embedpdf/angular/link';
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()) };
};
// Where the two links go on the first page, in page coordinates.
const TO_PAGE_3 = { x: 72, y: 24, width: 160, height: 28 };
const TO_WEBSITE = { x: 250, y: 24, width: 190, height: 28 };
@Component({
selector: 'demo-root',
imports: [
NgTemplateOutlet,
EpdfDocumentGate,
EpdfStage,
EpdfPageTemplate,
EpdfRenderLayer,
EpdfLinkLayer,
EpdfLinkTemplate,
],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(),
withRender(),
withInteraction(),
// The annotation plugin is only here to make the links below.
withAnnotation(),
withLink(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './custom.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<div class="toolbar">
<div class="segmented" role="group" aria-label="Link areas">
<button type="button" [attr.aria-pressed]="shown()" (click)="shown.set(true)">
Show link areas
</button>
<button type="button" [attr.aria-pressed]="!shown()" (click)="shown.set(false)">
As the PDF has them
</button>
</div>
</div>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-link-layer>
<ng-template epdfLink let-link let-native="native">
@if (shown()) {
<!-- Wrapping the layer's own anchor keeps what a click does. -->
<span class="pdf-link" [attr.data-kind]="link.target.kind">
<ng-container [ngTemplateOutlet]="native" />
</span>
} @else {
<!-- The layer's own, invisible area. -->
<ng-container [ngTemplateOutlet]="native" />
}
</ng-template>
</epdf-link-layer>
</ng-template>
</epdf-stage>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
private readonly annotation = inject(EpdfAnnotation);
private readonly document = inject(EpdfDocument);
private added = false;
protected readonly shown = signal(true);
constructor() {
// Made on load, since the document has none: a label the page shows, and a link over it.
effect(() => {
const [first, , third] = this.document.pages();
if (!first || !third || this.added) return;
this.added = true;
untracked(() => void this.addLinks(first.ref, third.ref));
});
}
private async addLinks(first: PageRef, third: PageRef) {
const label = (text: string, box: typeof TO_PAGE_3) =>
this.annotation.create(first, {
subtype: 'free-text',
box,
contents: text,
fontSize: 13,
fontColor: '#054fb3',
interiorColor: '#e8f1ff',
color: '#7db6ff',
strokeWidth: 1,
});
await label('Go to page 3 →', TO_PAGE_3);
await label('Open embedpdf.com ↗', TO_WEBSITE);
await this.annotation.create(first, {
subtype: 'link',
rect: TO_PAGE_3,
target: { kind: 'goto', destination: { kind: 'fit', page: third } },
});
await this.annotation.create(first, {
subtype: 'link',
rect: TO_WEBSITE,
target: { kind: 'uri', uri: 'https://www.embedpdf.com' },
});
}
}native is the layer’s own clickable area: wrap it to keep what a click does.
For a link you don’t want to change, show native alone.
Read the links on a page#
link.listLinks(page.ref); // every link on the page
link.getLinkAt(page.ref, { x: 120, y: 80 }); // the link at a point, or nullEach link has an id, its bounds on the page, and a target: where it goes.
target.kind | Goes to |
|---|---|
'goto' | A page of this document: target.destination |
'uri' | A website or an email address: target.uri |
'named' | A viewer action, such as the next page |
'javascript' | Runs the link’s script, when the actions plugin allows it |
'goto-remote', 'launch' | Nowhere: see Which links never run |
Follow a link from code#
activate() does what a click does:
const { outcome } = link.activate(someLink);It returns what happened right away, so a website can still open in a new tab: 'revealed' (the
view went to a page), 'uri' (a website opened), or 'dispatched' (the actions plugin runs the
link’s action). A link it won’t follow, such as one to another file, is 'reported'.
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
inject,
signal,
untracked,
ViewEncapsulation,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotation, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfLink, EpdfLinkLayer, withLink } from '@embedpdf/angular/link';
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()) };
};
// Where the links go on the first page, in page coordinates.
const TO_PAGE_3 = { x: 72, y: 24, width: 160, height: 28 };
const TO_WEBSITE = { x: 250, y: 24, width: 190, height: 28 };
const TO_SCRIPT = { x: 458, y: 24, width: 82, height: 28 };
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfLinkLayer],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
withStage(),
withRender(),
withInteraction(),
// The annotation plugin is only here to make the links below.
withAnnotation(),
withLink(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './list.css',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container *epdfDocumentGate="let document; fallback: loading">
<div class="layout">
<!-- The first page's links, read from the plugin, each with a button that follows it. -->
<aside class="panel">
<h3 class="heading">Links on page 1</h3>
<ul class="links">
@for (item of links(); track item.id) {
<li class="link">
<span class="kind">{{ item.target.kind }}</span>
<span class="label">{{ link.getLabel(item) }}</span>
<span class="where">at {{ round(item.bounds.x) }}, {{ round(item.bounds.y) }}</span>
<button type="button" class="button" (click)="link.activate(item)">Follow</button>
</li>
}
</ul>
<p class="last">
<code>activated$</code> {{ last() ?? 'not yet' }}
</p>
</aside>
<epdf-stage class="stage">
<ng-template epdfPage>
<epdf-render-layer />
<epdf-link-layer />
</ng-template>
</epdf-stage>
</div>
</ng-container>
<ng-template #loading><p class="loading">Loading…</p></ng-template>
`,
})
export class App {
protected readonly link = inject(EpdfLink);
private readonly annotation = inject(EpdfAnnotation);
private readonly document = inject(EpdfDocument);
private added = false;
protected readonly round = Math.round;
/** The list reads the links once they're made, and again each time a page's links are read. */
private readonly ready = signal(false);
private readonly reads = signal(0);
protected readonly links = computed(() => {
this.reads();
return this.ready() ? this.link.listLinks(0) : [];
});
protected readonly last = signal<string | null>(null);
constructor() {
// Every way of following a link ends up here: a click, a key, or code.
this.link.activated$
.pipe(takeUntilDestroyed())
.subscribe(({ target, activation }) => this.last.set(`${target.kind} → ${activation.outcome}`));
this.link.loaded$.pipe(takeUntilDestroyed()).subscribe(() => this.reads.update((n) => n + 1));
// Made on load, since the document has none: a label the page shows, and a link over it.
// The third one is a `javascript:` address, which a link never opens.
effect(() => {
const [first, , third] = this.document.pages();
if (!first || !third || this.added) return;
this.added = true;
untracked(() => void this.addLinks(first.ref, third.ref).then(() => this.ready.set(true)));
});
}
private async addLinks(first: PageRef, third: PageRef) {
const label = (text: string, box: typeof TO_PAGE_3) =>
this.annotation.create(first, {
subtype: 'free-text',
box,
contents: text,
fontSize: 13,
fontColor: '#054fb3',
interiorColor: '#e8f1ff',
color: '#7db6ff',
strokeWidth: 1,
});
await label('Go to page 3 →', TO_PAGE_3);
await label('Open embedpdf.com ↗', TO_WEBSITE);
await label('A script', TO_SCRIPT);
await this.annotation.create(first, {
subtype: 'link',
rect: TO_PAGE_3,
target: { kind: 'goto', destination: { kind: 'fit', page: third } },
});
await this.annotation.create(first, {
subtype: 'link',
rect: TO_WEBSITE,
target: { kind: 'uri', uri: 'https://www.embedpdf.com' },
});
await this.annotation.create(first, {
subtype: 'link',
rect: TO_SCRIPT,
target: { kind: 'uri', uri: 'javascript:alert(1)' },
});
}
}Methods#
Everything inject(EpdfLink) gives you.
| Method | What it does |
|---|---|
listLinks(page) | Every link on a page, once the page’s links are read. Read the links on a page |
getLink(page, id) | One link by its id, or null |
getLinkAt(page, point) | The link at a point on the page, the smallest when links overlap, or null |
listAllLinks() | Every link in the document, reading each page’s links as needed |
ensureLoaded(page) | Reads a page’s links if they aren’t read yet. <epdf-link-layer> does this for every page it shows. |
isLoaded(page), getStatus(page) | Whether a page’s links are read, and in detail: 'idle', 'loading', 'ready', 'error' or 'forbidden' |
activate(link) | Follows a link, or a link’s target, as a click does, and returns what happened. Follow a link from code |
activateAt(page, point) | Follows the link at a point on the page. Returns null when there’s no link there. |
resolve(target) | What following a target would do, without doing it: { kind } is 'destination', 'uri', 'named' or 'reported' |
getLabel(link) | A short label for a tooltip: the web address, or where the link goes |
Every method that returns a promise also takes a signal in its options, to cancel it.
Events#
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfLink, EpdfLinkLayer } from '@embedpdf/angular/link';
import { EpdfRenderLayer } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage } from '@embedpdf/angular/stage';
import { analytics } from './analytics';
@Component({
selector: 'app-reader',
imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfLinkLayer],
template: `
<epdf-stage>
<ng-template epdfPage>
<epdf-render-layer />
<epdf-link-layer />
</ng-template>
</epdf-stage>
`,
})
export class Reader {
constructor() {
inject(EpdfLink)
.activated$.pipe(takeUntilDestroyed())
.subscribe(({ target }) => analytics.track('link_followed', { kind: target.kind }));
}
}| Event | Fires when | Carries |
|---|---|---|
activated$ | Someone followed a link, by click, key or code | target, activation |
loaded$ | A page’s links were read, or read again after a change | page |
Good to know#
Which links never run#
A website link opens only with http, https, mailto or tel. An address without one opens on
https when it starts with // or www.; any other, such as /help or page2.html, is relative
to the document and opens nothing, so a link can never open a page of your own app. Links that
would start a program or open another file on the user’s computer are never followed, and a link’s
JavaScript runs only when the actions plugin has JavaScript turned on.
Links while editing annotations#
Links work while the pointer or pan tool is active. With the annotation plugin’s link tool,
the layer steps aside and links become annotations you can move, resize and retarget; see
Annotations.
Making links#
Links are annotations: create them with the annotation plugin, or with the engine; see Links.
Your feedback goes directly to the documentation team.