EmbedPDF

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:

link-layer.ts
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.

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:

custom-links.ts
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.

link.listLinks(page.ref); // every link on the page
link.getLinkAt(page.ref, { x: 120, y: 80 }); // the link at a point, or null

Each link has an id, its bounds on the page, and a target: where it goes.

target.kindGoes 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

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.

MethodWhat 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#

link-analytics.ts
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 }));
  }
}
EventFires whenCarries
activated$Someone followed a link, by click, key or codetarget, activation
loaded$A page’s links were read, or read again after a changepage

Good to know#

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 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.

Links are annotations: create them with the annotation plugin, or with the engine; see Links.

Was this page helpful?

Your feedback goes directly to the documentation team.