EmbedPDF

Signatures

A digital signature has two parts: the mark, what people see in the signature field, such as a drawn signature; and the key, which seals the document so any later change shows. The signature plugin signs with both, checks the signatures a document already has, and warns before an edit would break one.

Register withSignature() next to withForm() and withStamp(), with the key to sign with:

Loading live preview…

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  input,
  output,
  signal,
  untracked,
  viewChild,
  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 { EpdfForm, EpdfFormLayer, withForm } from '@embedpdf/angular/form';
import { EpdfStamp, withStamp } from '@embedpdf/angular/stamp';
import { createTestSigner, EpdfSignature, withSignature } from '@embedpdf/angular/signature';
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
const assetEngine = engine; // a person's marks are stamps, kept as PDFs; they open here too
// A throwaway key for the demo. Bring your own with `webCryptoSigner`, a
// service with `remoteSigner`, or a person's own with `personalSigner`.
const signer = createTestSigner({ commonName: 'Ada Lovelace' });

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()) };
};

// A mark's button: its picture once there is one, its label until then.
@Component({
  selector: 'button[demoMarkButton]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { type: 'button', class: 'button', '[title]': 'label()', '(click)': 'pick.emit()' },
  template: `
    @if (url(); as url) {
      <img [src]="url" [alt]="label()" class="preview" />
    } @else {
      {{ label() }}
    }
  `,
})
export class MarkButton {
  readonly assetId = input.required<string>();
  readonly label = input.required<string>();
  readonly pick = output();
  protected readonly url = inject(EpdfStamp).previewUrlOf(() => this.assetId());
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfFormLayer,
    MarkButton,
  ],
  providers: [
    provideEmbedPdf(
      { engine, initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withForm(),
      withStamp({ assetEngine }),
      withSignature({
        key: () => signer,
        // Trust the demo key itself, so its signatures check out as 'valid'.
        trust: { anchors: async () => [(await signer).certificate] },
      }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './basic.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div class="toolbar">
        @for (asset of person()?.signatures; track asset.id) {
          <button demoMarkButton [assetId]="asset.id" [label]="asset.label" (pick)="sign(asset.id)"></button>
        }
        <output class="readout">{{ status() }}</output>
      </div>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <!-- An empty signature field is "sign here": a click makes it the target -->
          <epdf-form-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly form = inject(EpdfForm);
  private readonly stamp = inject(EpdfStamp);
  private readonly signature = inject(EpdfSignature);
  private readonly document = inject(EpdfDocument);
  private readonly stage = viewChild(EpdfStage);
  private done = false;

  protected readonly person = computed(() => this.signature.signerRows()[0]);
  private readonly field = computed(() =>
    this.form.fields().find((candidate) => candidate.family === 'signature'),
  );
  private readonly error = signal<string | null>(null);

  protected readonly status = computed(() => {
    const signed = this.signature.signatures()[0];
    const error = this.error();
    if (!this.person() || !this.field()) return 'Getting ready…';
    if (error) return error;
    if (this.signature.busy()) return 'Signing…';
    if (signed) return `Signed by ${signed.signer.name}: ${signed.verdict?.summary ?? 'checking…'}`;
    if (this.signature.target()) return 'Now pick the signature';
    return 'Click the field, or pick the signature';
  });

  constructor() {
    // The ebook has no signature field, and the browser holds no signatures yet: add a field
    // to the last page, and one person with a typed signature.
    effect(() => {
      const page = this.document.pages().at(-1)?.ref;
      const stage = this.stage();
      if (this.form.status() !== 'ready' || !page || !stage || this.done) return;
      this.done = true;
      untracked(() => void this.setUp(page, stage));
    });
  }

  /** The target is the field someone clicked; without one, the form's signature field. */
  protected sign(assetId: string) {
    const destination = this.signature.target() ?? this.field()?.ref;
    if (!destination) return;
    this.error.set(null);
    this.signature
      .placeMark({ assetId }, { field: destination })
      .catch((reason: Error) => this.error.set(reason.message));
  }

  private async setUp(page: PageRef, stage: EpdfStage) {
    await this.form.create({
      family: 'signature',
      name: 'approval',
      widgets: [{ page, rect: { x: 72, y: 560, width: 220, height: 64 }, color: '#94a3b8' }],
    });
    stage.goToPage(page);
    const { library } = await this.stamp.createLibrary('Ada Lovelace', { kind: 'signatures' });
    await this.stamp.createAsset({
      libraryId: library.id,
      name: 'signature',
      label: 'Signature',
      mark: { kind: 'text', text: 'Ada Lovelace', fontFamily: 'times-italic', color: '#1d2b53' },
    });
  }
}
signature-plugin.ts
import { Component } from '@angular/core';
import { withForm } from '@embedpdf/angular/form';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { indexedDbKeyStore, personalSigner, withSignature } from '@embedpdf/angular/signature';
import { withStamp } from '@embedpdf/angular/stamp';
import { rootCertificate } from './certificates';
import { engine } from './pdf';

@Component({
  selector: 'app-signing',
  providers: [
    provideEmbedPdf(
      { engine },
      /* … */
      withForm(),
      withStamp(),
      withSignature({
        key: () => personalSigner({ subject: 'Ada Lovelace', store: indexedDbKeyStore('my-app-keys') }),
        trust: { anchors: async () => [rootCertificate] }, // the certificates you trust
      }),
    ),
  ],
  template: `<!-- … -->`,
})
export class Signing {}

Sign a field#

const { version } = await signature.sign({
  field: toFieldRef('approval'),
  mark: { assetId },
  signer: { reason: 'Approved', location: 'Amsterdam' },
});

The mark becomes what the field shows, the key seals the document, and the signed document is saved as a new version. If anything fails along the way, nothing is left half-signed.

A mark is a stamp from a person’s signature library, or image bytes you bring:

{ assetId } // a stamp, drawn, typed or uploaded; see "A person's signatures" below
{ source: pngBytes } // a PNG, a JPEG, or a one-page PDF

Let people click to sign#

People can also arm their signature and click where it goes, as in Preview. With the stamp plugin, arm a mark from a signature library:

  • over an empty signature field, the click signs it;
  • over a signed field, nothing happens;
  • anywhere else, the mark is placed as a stamp.

placeMark() does the same from code:

await signature.placeMark({ assetId }, { field: toFieldRef('approval') }); // signs the field
await signature.placeMark({ assetId }, { page: page.ref, center: { x: 120, y: 90 } }); // a stamp

What happens in a field depends on mode: 'sign' signs it, 'visual' only draws the mark in, and 'ask' asks your UI first through signRequested$, so you can show your own sign dialog.

Loading live preview…

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  untracked,
  viewChild,
  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 { EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfForm, EpdfFormLayer, withForm } from '@embedpdf/angular/form';
import { EpdfStamp, withStamp } from '@embedpdf/angular/stamp';
import { createTestSigner, EpdfSignature, withSignature } from '@embedpdf/angular/signature';
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
const assetEngine = engine; // a person's marks are stamps, kept as PDFs; they open here too
const signer = createTestSigner({ commonName: 'Ada Lovelace' });

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: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfAnnotationLayer,
    EpdfFormLayer,
  ],
  providers: [
    provideEmbedPdf(
      { engine, initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
      withForm(),
      withStamp({ assetEngine }),
      withSignature({ key: () => signer }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './click-to-sign.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div class="toolbar">
        <button
          type="button"
          class="button"
          [disabled]="!mark()"
          [attr.aria-pressed]="stamp.armedAsset() !== null"
          (click)="toggleArmed()"
        >
          {{ stamp.armedAsset() ? 'Disarm' : 'Arm the signature' }}
        </button>
        <output class="readout">{{ hint() }}</output>
      </div>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-annotation-layer />
          <epdf-form-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  protected readonly stamp = inject(EpdfStamp);
  private readonly form = inject(EpdfForm);
  private readonly signature = inject(EpdfSignature);
  private readonly document = inject(EpdfDocument);
  private readonly stage = viewChild(EpdfStage);
  private done = false;

  protected readonly mark = computed(() => this.signature.signerRows()[0]?.signatures[0]);

  protected readonly hint = computed(() => {
    const signed = this.signature.signatures()[0];
    if (signed) return `The field is signed by ${signed.signer.name}`;
    if (this.stamp.armedAsset()) return 'Click the empty field to sign it, or anywhere else to place it';
    if (this.mark()) return 'Arm the signature, then click where it goes';
    return 'Getting ready…';
  });

  constructor() {
    // On load: a signature field on the last page, and a person's signature, armed.
    effect(() => {
      const page = this.document.pages().at(-1)?.ref;
      const stage = this.stage();
      if (this.form.status() !== 'ready' || !page || !stage || this.done) return;
      this.done = true;
      untracked(() => void this.setUp(page, stage));
    });
  }

  protected toggleArmed() {
    const mark = this.mark();
    if (this.stamp.armedAsset()) this.stamp.disarm();
    else if (mark) void this.stamp.armAsset(mark.id);
  }

  private async setUp(page: PageRef, stage: EpdfStage) {
    await this.form.create({
      family: 'signature',
      name: 'approval',
      widgets: [{ page, rect: { x: 72, y: 560, width: 220, height: 64 }, color: '#94a3b8' }],
    });
    stage.goToPage(page);
    const { library } = await this.stamp.createLibrary('Ada Lovelace', { kind: 'signatures' });
    const { asset } = await this.stamp.createAsset({
      libraryId: library.id,
      name: 'signature',
      label: 'Signature',
      mark: { kind: 'text', text: 'Ada Lovelace', fontFamily: 'times-italic', color: '#1d2b53' },
    });
    await this.stamp.armAsset(asset.id, { targetWidth: 160 });
  }
}

Fill a field without signing#

Sometimes a signature only needs to look signed, such as for a form that gets printed. Draw the mark into the field without sealing anything:

await signature.fillField(toFieldRef('approval'), { source: pngBytes });
await signature.clearField(toFieldRef('approval'));

A signed field can’t be filled or cleared: what it shows is part of what was signed.

Loading live preview…

import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  signal,
  untracked,
  viewChild,
  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 { EpdfForm, toFieldRef, withForm } from '@embedpdf/angular/form';
import { EpdfStamp, withStamp } from '@embedpdf/angular/stamp';
import { EpdfSignature, withSignature } from '@embedpdf/angular/signature';
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
const assetEngine = engine; // a person's marks are stamps, kept as PDFs; they open here too

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()) };
};

const APPROVAL = toFieldRef('approval');

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
  providers: [
    provideEmbedPdf(
      { engine, initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withForm(),
      withStamp({ assetEngine }),
      // No key: a mark is only drawn in, nothing is sealed.
      withSignature(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './fill.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div class="toolbar">
        <button type="button" class="button" [disabled]="!assetId() || drawn()" (click)="drawIn()">
          Draw it in
        </button>
        <button type="button" class="button" [disabled]="!drawn()" (click)="takeOut()">
          Take it out
        </button>
        <output class="readout">
          {{ drawn() ? 'Drawn in, not signed: no key sealed anything' : 'The field is empty' }}
        </output>
      </div>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly form = inject(EpdfForm);
  private readonly stamp = inject(EpdfStamp);
  private readonly signature = inject(EpdfSignature);
  private readonly document = inject(EpdfDocument);
  private readonly stage = viewChild(EpdfStage);
  private done = false;
  protected readonly assetId = signal<string | null>(null);
  protected readonly drawn = signal(false);

  constructor() {
    this.signature.filled$.pipe(takeUntilDestroyed()).subscribe(() => this.drawn.set(true));
    this.signature.cleared$.pipe(takeUntilDestroyed()).subscribe(() => this.drawn.set(false));

    // On load: a signature field on the last page, a typed signature, drawn into the field.
    effect(() => {
      const page = this.document.pages().at(-1)?.ref;
      const stage = this.stage();
      if (this.form.status() !== 'ready' || !page || !stage || this.done) return;
      this.done = true;
      untracked(() => void this.setUp(page, stage));
    });
  }

  protected drawIn() {
    const assetId = this.assetId();
    if (assetId) void this.signature.fillField(APPROVAL, { assetId });
  }

  protected takeOut() {
    void this.signature.clearField(APPROVAL);
  }

  private async setUp(page: PageRef, stage: EpdfStage) {
    await this.form.create({
      family: 'signature',
      name: 'approval',
      widgets: [{ page, rect: { x: 72, y: 560, width: 220, height: 64 }, color: '#94a3b8' }],
    });
    stage.goToPage(page);
    const { library } = await this.stamp.createLibrary('Ada Lovelace', { kind: 'signatures' });
    const { asset } = await this.stamp.createAsset({
      libraryId: library.id,
      name: 'signature',
      label: 'Signature',
      mark: { kind: 'text', text: 'Ada Lovelace', fontFamily: 'times-italic', color: '#1d2b53' },
    });
    this.assetId.set(asset.id);
    await this.signature.fillField(APPROVAL, { assetId: asset.id });
  }
}

The key#

The engine never holds a key. You give the plugin a signer, which does the signing. @embedpdf/angular/signature has one for each case:

// Your own key and certificate chain
webCryptoSigner({ privateKey, certificateChain: [leaf, issuer] });
 
// Your signing service or hardware module: only the document's fingerprint leaves the browser
remoteSigner({ sign: ({ digest, algorithm, subFilter }) => api.sign(digest, algorithm, subFilter) });
 
// A key per person, created in the browser and kept there, never readable
personalSigner({ subject: 'Ada Lovelace', store: indexedDbKeyStore('my-app-keys') });
 
// For tests and demos
createTestSigner({ commonName: 'Demo' });

A personal signer works like signing in Preview, with real cryptography: a reader who trusts that person’s certificate sees a valid signature, and everyone else sees “validity unknown”, the honest answer for a key the person made themselves. For signatures everyone can check, sign with a certificate from a trusted provider, through webCryptoSigner or remoteSigner.

Signing in two steps#

When the key signs somewhere else, such as on a phone or in a signing service that asks for approval, split the signing in two:

const { signingId, digest } = await signature.prepareSignature({ field, mark: { assetId } });
const cms = await myService.sign(digest); // may take minutes
await signature.completeSignature(signingId, cms);

While a signature is prepared, the document can’t be changed, so nothing can slip in between. signature.cancelPending() gives up.

Check the signatures#

signature-list.ts
import { Component, inject } from '@angular/core';
import { EpdfSignature } from '@embedpdf/angular/signature';

@Component({
  selector: 'app-signature-list',
  template: `
    <ul>
      @for (sig of signature.signatures(); track sig.fieldName) {
        <li>{{ sig.fieldName }}: {{ sig.verdict?.summary ?? 'checking…' }}</li>
      }
    </ul>
  `,
})
export class SignatureList {
  protected readonly signature = inject(EpdfSignature);
}

A verdict’s summary is:

SummaryMeans
'valid'Nothing changed since it was signed, and you trust the signer
'valid-untrusted'Nothing changed, but the signer’s certificate isn’t one you trust
'invalid'The document changed in a way the signature doesn’t allow
'indeterminate'It couldn’t be checked, for example because a certificate is missing

The plugin checks the signatures when the document opens, and again after every change. For a detailed report of what changed since a signature, rule by rule, use signature.analyzeChanges().

Warn before an edit breaks a signature#

After an approval signature, filling in the form and adding more signatures keep it valid, but an annotation doesn’t. The plugin warns you the moment a change would break a signature once it’s saved, as Acrobat does:

break-warning.ts
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfSignature } from '@embedpdf/angular/signature';
import { toast } from './toast';

@Component({ selector: 'app-signature-guard', template: '' })
export class SignatureGuard {
  private readonly signature = inject(EpdfSignature);

  constructor() {
    this.signature.invalidationPredicted$.pipe(takeUntilDestroyed()).subscribe(({ field }) => {
      const name = this.signature.getSignature(field)?.fieldName;
      toast(`This change will break the signature in "${name}" when saved.`);
    });
  }
}

signature.getProtection() tells you what the document’s signatures allow, before anyone tries. A signer who wants comments to stay allowed signs with a certification that allows them.

Loading live preview…

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  signal,
  untracked,
  viewChild,
  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,
  EpdfAnnotationLayer,
  withAnnotation,
} from '@embedpdf/angular/annotation';
import { EpdfForm, toFieldRef, withForm } from '@embedpdf/angular/form';
import { EpdfStamp, withStamp } from '@embedpdf/angular/stamp';
import { createTestSigner, EpdfSignature, withSignature } from '@embedpdf/angular/signature';
import { localEngine } from '@embedpdf/engine';

const engine = localEngine();
const assetEngine = engine; // a person's marks are stamps, kept as PDFs; they open here too
const signer = createTestSigner({ commonName: 'Ada Lovelace' });

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: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfAnnotationLayer],
  providers: [
    provideEmbedPdf(
      { engine, initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      withAnnotation(),
      withForm(),
      withStamp({ assetEngine }),
      withSignature({
        key: () => signer,
        // Trust the demo key itself, so its signature checks out as 'valid'.
        trust: { anchors: async () => [(await signer).certificate] },
      }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './break-warning.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div class="toolbar">
        <button type="button" class="button" [disabled]="!signed() || !page()" (click)="drawBox()">
          Draw a box on the page
        </button>
        <output class="readout">{{ status() }}</output>
        @if (warning(); as warning) {
          <output class="warning">{{ warning }}</output>
        }
      </div>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-annotation-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly form = inject(EpdfForm);
  private readonly stamp = inject(EpdfStamp);
  private readonly annotation = inject(EpdfAnnotation);
  private readonly signature = inject(EpdfSignature);
  private readonly document = inject(EpdfDocument);
  private readonly stage = viewChild(EpdfStage);
  private done = false;

  /** The last page, which has room for the field. */
  protected readonly page = computed(() => this.document.pages().at(-1)?.ref);
  protected readonly signed = computed(() => this.signature.signatures()[0]);
  protected readonly status = computed(() => {
    const signed = this.signed();
    return signed
      ? `Signed by ${signed.signer.name}: ${signed.verdict?.summary ?? 'checking…'}`
      : 'Signing…';
  });
  protected readonly warning = signal<string | null>(null);

  constructor() {
    // The moment a change would break a signature once it's saved, as Acrobat warns.
    this.signature.invalidationPredicted$.pipe(takeUntilDestroyed()).subscribe(({ field }) => {
      const name = this.signature.getSignature(field)?.fieldName;
      this.warning.set(`This change will break the signature in "${name}" when saved.`);
    });

    // On load: a signature field on the last page, signed by Ada with a typed signature.
    effect(() => {
      const page = this.page();
      const stage = this.stage();
      if (this.form.status() !== 'ready' || !page || !stage || this.done) return;
      this.done = true;
      untracked(() => void this.signDocument(page, stage));
    });
  }

  protected drawBox() {
    const page = this.page();
    if (!page) return;
    void this.annotation.create(page, {
      subtype: 'square',
      box: { x: 320, y: 560, width: 140, height: 64 },
      color: '#e11d48',
      strokeWidth: 2,
    });
  }

  private async signDocument(page: PageRef, stage: EpdfStage) {
    await this.form.create({
      family: 'signature',
      name: 'approval',
      widgets: [{ page, rect: { x: 72, y: 560, width: 220, height: 64 } }],
    });
    stage.goToPage(page);
    const { library } = await this.stamp.createLibrary('Ada Lovelace', { kind: 'signatures' });
    const { asset } = await this.stamp.createAsset({
      libraryId: library.id,
      name: 'signature',
      label: 'Signature',
      mark: { kind: 'text', text: 'Ada Lovelace', fontFamily: 'times-italic', color: '#1d2b53' },
    });
    await this.signature.sign({ field: toFieldRef('approval'), mark: { assetId: asset.id } });
  }
}

A person’s signatures#

A person’s signature and initials are stamps, in a library of kind 'signatures', drawn, typed or uploaded with the stamp plugin:

const { library } = await stamp.createLibrary('Ada Lovelace', { kind: 'signatures' });
await stamp.createAsset({ libraryId: library.id, name: 'signature', label: 'Signature', mark: { kind: 'ink', strokes } });
await stamp.createAsset({
  libraryId: library.id,
  name: 'initials',
  label: 'Initials',
  mark: { kind: 'text', text: 'AL', fontFamily: 'times-italic' },
});

signature.signerRows() lists each person with their signature and initials, for a picker. The library is a normal PDF, so Acrobat can open it too.

”Sign here” in a form#

With the form layer, an empty signature field shows a “sign here” button. Clicking it makes the field the target: the next mark the person picks goes there. A signed field, clicked, asks your UI to show its details:

signature.setTarget(toFieldRef('approval')); // the next mark goes here; null clears
EventFires whenShow
targetChanged$A field became the targetyour signatures panel
signRequested$In 'ask' mode, a mark was dropped on a fieldyour sign dialog
inspectionRequested$A signed field was clickedthe signature’s details

Methods#

Everything inject(EpdfSignature) gives you.

MethodWhat it does
sign({ field, mark })Signs a field: the mark becomes what it shows, and the key seals the document. Also takes signer, a per-call key, certify and lock, and resolves { signature, version, protection }. Sign a field
placeMark(mark, { field }), placeMark(mark, { page, center })Does what a click with an armed mark does: in a field, signs, fills or asks, by mode; anywhere else, places a stamp. Resolves what it did as kind: 'signed', 'filled', 'requested' or 'placed'. Let people click to sign
fillField(field, mark), clearField(field)Draws a mark into a field without signing, or takes it out again. Refused on a signed field. Fill a field without signing
prepareSignature({ field, mark }), completeSignature(signingId, cms)Signs in two steps: the first resolves { signingId, digest } for your key to sign, the second seals the document with what the key returned. Signing in two steps
cancelPending()Gives up a signature prepared in two steps, so the document can be changed again
validate({ until? })Checks every signature again and resolves the verdicts. until: 'persisted' checks the file as it was opened. Which bytes are checked
validateField(field)Checks one signed field and resolves its verdict
getVerdict(field), listVerdicts()One signed field’s verdict from the last check, or every verdict; null before the first check. Check the signatures
getSignature(field)One signature field’s facts, signed or not, such as fieldName, signed and signer, or null
listUnsignedFields()The signature fields still to sign, in document order
analyzeChanges({ since })What changed after a signature ({ signatureIndex }) or a version of the file ({ revisionIndex }), rule by rule
readRevision(field), readRevision({ revisionIndex })The exact bytes of the file version a signature covers. The signed bytes
setTarget(field)Makes a field the target, where the next mark the person picks goes; null clears. “Sign here” in a form
requestInspection(field)Asks your UI to show a signed field’s details, as clicking it does: inspectionRequested$ fires
getMode()The mode in effect: the mode setting, or its default. Settings
getSnapshot()Everything at once, as the engine reads it: every signature field, signed or not, the file’s versions, and protection
refresh()Reads the signatures again. You rarely need it: the plugin reads and checks them again after every change.
canSign(field), canCertify(), canFill(field), canReadRevision()Whether signing a field, certifying, drawing a mark into a field without signing, and reading the signed bytes are allowed. Permissions
getSettings(), updateSettings(changes), resetSettings()Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use inject(EpdfSignature).settings(). Settings

field is a field’s ref, such as toFieldRef('approval'); the methods that only read also take the ref of the field’s box. Every method that returns a promise also takes a signal in its options, to cancel it.

State#

Every value is a signal on inject(EpdfSignature): read it in a template or a computed(), and your view updates when it changes. The method in the last column reads the same value once.

FieldWhat it isRead once
signatures()The signed fields, in document order, each with its verdict once checkedlistSignatures()
protection()What the document’s signatures allow, or null before they’re readgetProtection()
target()The field the next mark goes to, or nullgetTarget()
busy()Whether a signing or a fill is runningisBusy()
pending()A signature prepared in two steps and not completed yet, as { signingId, field }, or null. Until it’s completed or cancelled, the document can’t be changed.getPending()
status()Whether the signatures are read: 'idle', 'loading', 'ready', 'forbidden' or 'error'getStatus()

Settings#

Set them when you register the plugin, and change them while the app runs with signature.updateSettings({ … }); signature.resetSettings() goes back to what you registered, and inject(EpdfSignature).settings() gives you the current settings.

SettingWhat it doesDefault
keyThe signer, or a function that returns one when someone signsnone
modeWhat a mark does in a field: 'sign', 'visual' or 'ask''sign' with a key, else 'visual'
trustThe certificates you trust, for checking signaturesnone
allowCertifyLet a first signature be a certification, which sets what stays allowedfalse

Events#

save-warning.ts
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfSignature } from '@embedpdf/angular/signature';
import { showWarning } from './warnings';

@Component({ selector: 'app-save-warning', template: '' })
export class SaveWarning {
  constructor() {
    inject(EpdfSignature)
      .invalidationPredicted$.pipe(takeUntilDestroyed())
      .subscribe(() => showWarning('Saving this change will break a signature.'));
  }
}
EventFires whenCarries
signed$A field was signed, whoever signed itfield, result, origin
filled$, cleared$A mark was drawn into a field, or removedfield
validated$The signatures were checkedverdicts
protectionChanged$What the signatures allow changedprotection
invalidationPredicted$A change would break a signature once savedfield, detail
targetChanged$A field became the target, or the target was clearedfield (null when cleared)
signRequested$In 'ask' mode, a mark was dropped on a fieldfield, mark
inspectionRequested$A signed field was clickedfield
settingsChanged$The settings changedsettings, changed

result is what sign() resolves. “Sign here” in a form says what to show for the last three.

Permissions#

PermissionWhat it allowsCheck
doc.signSigningsignature.canSign(field)
doc.sign.certifySigning a certificationsignature.canCertify()
doc.forms.fillDrawing a mark into a field without signingsignature.canFill(field)
doc.downloadReading the exact bytes a signature coverssignature.canReadRevision()

A user with a fields:sign (fields:fill) permission for one group signs (fills in) that group’s fields only, so the checks take the field. See Signers and groups. On a signed document, the engine also refuses what its signatures forbid.

Good to know#

Which bytes are checked#

The plugin checks the document as it is in the viewer, with changes that haven’t been downloaded yet, so the verdict is the one the downloaded file will get. validate({ until: 'persisted' }) checks the file as it was opened.

The signed bytes#

signature.readRevision({ revisionIndex }) gives you the exact version of the file a signature covers, for an audit trail.

How signatures are stored, and what each certification allows, is on Digital signatures.

Was this page helpful?

Your feedback goes directly to the documentation team.