EmbedPDF

Metadata

A PDF carries information about itself: a title, an author, when it was made. Register withMetadata() to show it in your UI and let people change it:

title-field.ts
import { Component, computed, inject } from '@angular/core';
import { EpdfMetadata } from '@embedpdf/angular/metadata';

@Component({
  selector: 'app-title-field',
  template: `
    <label>
      Title
      <input
        #input
        [value]="title()"
        [disabled]="!metadata.canUpdate()"
        (blur)="metadata.update({ title: input.value })"
      />
    </label>
  `,
})
export class TitleField {
  protected readonly metadata = inject(EpdfMetadata);
  protected readonly title = computed(() => this.metadata.fields()?.title ?? '');
}

What inject(EpdfMetadata) gives you updates by itself: after your own changes, and on a shared document, after someone else’s. Nothing is read when a document opens: the metadata loads the first time something asks for it, so a document nobody shows the properties of costs nothing.

Read#

metadata.fields() has every standard field. A field the file doesn’t set is null:

FieldWhat it is
titleThe document’s title
authorWho wrote it
subjectWhat it’s about
keywordsKeywords, as one string (often separated by commas)
creatorThe app the document was made in
producerThe app that turned it into a PDF
createdAtWhen it was created, as an ISO date
modifiedAtWhen it was last changed, as an ISO date
trappedA print-shop setting: 'true', 'false' or 'unknown', never null

status says whether the metadata hasn’t been asked for yet ('idle'), is loading ('loading'), is there ('ready'), or is not allowed ('forbidden'). Reading the state is what asks: EpdfMetadata loads the metadata of the document it reads, so its first answer is 'loading'. Show that while it lasts, the way you show any other load:

const title = status === 'ready' ? (metadata?.title ?? 'Untitled') : 'Loading…';

From code, ensureLoaded() reads it and resolves once it’s there:

await metadata.ensureLoaded();
const author = metadata.getSnapshot()?.author;

Every field inject(EpdfMetadata) gives you, and every status, is listed under State.

Loading live preview…

import { ChangeDetectionStrategy, Component, inject, ViewEncapsulation } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { DocumentMetadata, OpenInput } from '@embedpdf/angular/runtime';
import { EpdfMetadata, withMetadata } from '@embedpdf/angular/metadata';
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()) };
};

const FIELDS = [
  ['title', 'Title'],
  ['author', 'Author'],
  ['subject', 'Subject'],
  ['keywords', 'Keywords'],
  ['creator', 'Made in'],
  ['producer', 'Turned into a PDF by'],
  ['createdAt', 'Created'],
  ['modifiedAt', 'Last changed'],
  ['trapped', 'Trapped'],
] as const;

// Dates are ISO strings: show them in the reader's own format.
const DATES: ReadonlySet<string> = new Set(['createdAt', 'modifiedAt']);
const show = (field: string, value: string | null) => {
  if (value === null) return null;
  return DATES.has(field) ? new Date(value).toLocaleString() : value;
};

// The document's properties, the way a "Document properties" dialog shows them.
@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withMetadata(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './properties.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <section *epdfDocumentGate="let document; fallback: loading" class="panel">
      <header class="panel-header">
        <h3 class="panel-title">Document properties</h3>
        <span class="status" [attr.data-status]="metadata.status()">
          {{ metadata.status() }}
        </span>
      </header>
      <dl class="properties">
        @for (entry of fields; track entry[0]) {
          @let value = valueOf(entry[0], metadata.fields());
          <div class="property">
            <dt>{{ entry[1] }}</dt>
            <dd [class.unset]="value === null">{{ value ?? 'not set' }}</dd>
          </div>
        }
      </dl>
    </section>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  protected readonly metadata = inject(EpdfMetadata);
  protected readonly fields = FIELDS;

  protected valueOf(field: (typeof FIELDS)[number][0], metadata: DocumentMetadata | null) {
    return metadata ? show(field, metadata[field]) : null;
  }
}

Change#

update() changes the fields you pass, and null removes one:

await metadata.update({
  title: 'Q2 Proposal (final)',
  author: 'Dana Smith',
  subject: null,
});

It resolves with { metadata }: the fields after the change. The change is part of the document, kept when it’s downloaded.

Loading live preview…

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  input,
  OnInit,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { DocumentMetadata, OpenInput } from '@embedpdf/angular/runtime';
import { EpdfMetadata, withMetadata } from '@embedpdf/angular/metadata';
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()) };
};

const EDITABLE = ['title', 'author', 'subject', 'keywords'] as const;
type Draft = Record<(typeof EDITABLE)[number], string>;

// A properties form: the inputs start with what the file says, and Save writes them back.
@Component({
  selector: 'demo-properties-form',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="layout">
      <form class="form" (submit)="$event.preventDefault(); save()">
        @for (key of editable; track key) {
          <label class="row">
            <span class="name">{{ key }}</span>
            <input
              #field
              class="field"
              placeholder="not set"
              [value]="draft()[key]"
              [disabled]="!metadata.canUpdate()"
              (input)="edit(key, field.value)"
            />
          </label>
        }
        <div class="actions">
          <button type="submit" class="button primary" [disabled]="!metadata.canUpdate()">
            Save
          </button>
          <button
            type="button"
            class="button"
            [disabled]="!metadata.canUpdate()"
            (click)="modifiedNow()"
          >
            Set the modified date to now
          </button>
        </div>
      </form>
      <section class="changes" aria-live="polite">
        <h3 class="changes-title">Changes</h3>
        <p class="modified">Last changed: {{ modifiedAt() }}</p>
        @if (changes().length === 0) {
          <p class="empty">Edit a field and save.</p>
        } @else {
          <ul class="log">
            @for (change of changes(); track changes().length - $index) {
              <li>{{ change }}</li>
            }
          </ul>
        }
      </section>
    </div>
  `,
})
export class PropertiesForm implements OnInit {
  /** The fields as the file has them when the form starts. */
  readonly initial = input.required<DocumentMetadata>();
  protected readonly metadata = inject(EpdfMetadata);
  protected readonly editable = EDITABLE;
  protected readonly draft = signal<Draft>({ title: '', author: '', subject: '', keywords: '' });
  protected readonly changes = signal<string[]>([]);
  protected readonly modifiedAt = computed(() => {
    const modifiedAt = this.metadata.fields()?.modifiedAt ?? null;
    return modifiedAt ? new Date(modifiedAt).toLocaleString() : 'not set';
  });

  constructor() {
    // Every change to the standard fields, by you or anyone else.
    this.metadata.updated$
      .pipe(takeUntilDestroyed())
      .subscribe(({ changedKeys, origin }) =>
        this.changes.update((list) => [
          `${changedKeys.join(', ')} changed ${origin.kind === 'local' ? 'here' : 'elsewhere'}`,
          ...list,
        ]),
      );
  }

  ngOnInit() {
    const initial = this.initial();
    this.draft.set({
      title: initial.title ?? '',
      author: initial.author ?? '',
      subject: initial.subject ?? '',
      keywords: initial.keywords ?? '',
    });
  }

  protected edit(key: keyof Draft, value: string) {
    this.draft.update((draft) => ({ ...draft, [key]: value }));
  }

  // An empty field removes it: null, not ''.
  protected save() {
    const draft = this.draft();
    void this.metadata.update({
      title: draft.title.trim() || null,
      author: draft.author.trim() || null,
      subject: draft.subject.trim() || null,
      keywords: draft.keywords.trim() || null,
    });
  }

  protected modifiedNow() {
    void this.metadata.update({ modifiedAt: new Date() });
  }
}

// The form starts once the properties have been read from the file.
@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, PropertiesForm],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withMetadata(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './edit.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      @if (metadata.fields(); as fields) {
        <demo-properties-form [initial]="fields" />
      } @else {
        <p class="loading">Reading…</p>
      }
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  protected readonly metadata = inject(EpdfMetadata);
}

Your own fields#

Keep your own values in the document, such as a contract number or who reviewed it. PDF apps show them as custom properties:

custom-fields.ts
import { Component, inject } from '@angular/core';
import { EpdfMetadata } from '@embedpdf/angular/metadata';

@Component({
  selector: 'app-contract-number',
  template: `<span>Contract {{ metadata.custom.fields()?.contractId }}</span>`,
})
export class ContractNumber {
  // metadata.custom.fields() is { contractId: 'C-2026-114', reviewedBy: 'dana' }
  protected readonly metadata = inject(EpdfMetadata);
}
await metadata.custom.update({ reviewedBy: 'lee', draftOwner: null });

A string sets a field, null removes it, and fields you leave out stay as they are. A field name is 1 to 127 plain characters and can’t be a standard field’s name.

Loading live preview…

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, isPluginError, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfMetadata, withMetadata } from '@embedpdf/angular/metadata';
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()) };
};

// Your own fields, kept in the document: add one, change one, remove one.
@Component({
  selector: 'demo-custom-fields',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <section class="panel">
      <table class="fields">
        <thead>
          <tr>
            <th>Name</th>
            <th>Value</th>
            <th aria-label="Remove"></th>
          </tr>
        </thead>
        <tbody>
          @for (field of fields(); track field[0]) {
            <tr>
              <td class="key">{{ field[0] }}</td>
              <td>{{ field[1] }}</td>
              <td>
                <!-- null removes a field; the others stay as they are. -->
                <button
                  type="button"
                  class="button quiet"
                  [disabled]="!metadata.canUpdate()"
                  (click)="remove(field[0])"
                >
                  Remove
                </button>
              </td>
            </tr>
          }
        </tbody>
      </table>
      <form class="add" (submit)="$event.preventDefault(); add()">
        <input
          #nameField
          class="field"
          aria-label="Name"
          placeholder="Name, such as approvedOn"
          [value]="name()"
          (input)="name.set(nameField.value)"
        />
        <input
          #valueField
          class="field"
          aria-label="Value"
          placeholder="Value"
          [value]="value()"
          (input)="value.set(valueField.value)"
        />
        <button type="submit" class="button" [disabled]="!metadata.canUpdate() || !name().trim()">
          Add
        </button>
      </form>
      @if (error(); as error) {
        <p class="error">{{ error }}</p>
      }
    </section>
  `,
})
export class CustomFields {
  protected readonly metadata = inject(EpdfMetadata);
  protected readonly fields = computed(() => Object.entries(this.metadata.custom.fields() ?? {}));
  protected readonly name = signal('');
  protected readonly value = signal('');
  protected readonly error = signal<string | null>(null);

  constructor() {
    // Two fields of our own, written on load. Setting the same values again changes nothing.
    void this.metadata.custom.update({ contractId: 'C-2026-114', reviewedBy: 'dana' });
  }

  protected async add() {
    try {
      await this.metadata.custom.update({ [this.name().trim()]: this.value() });
      this.name.set('');
      this.value.set('');
      this.error.set(null);
    } catch (failure) {
      // A name the PDF can't take, such as one of the standard fields.
      if (isPluginError(failure, 'invalid-input')) this.error.set(failure.message);
      else throw failure;
    }
  }

  protected remove(key: string) {
    void this.metadata.custom.update({ [key]: null });
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, CustomFields],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withMetadata(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './custom-fields.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <demo-custom-fields *epdfDocumentGate="let document; fallback: loading" />

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {}

Methods#

Everything inject(EpdfMetadata) gives you.

MethodWhat it does
update(changes)Changes the standard fields you pass; null removes one. Resolves { metadata }, the fields after the change. Change
custom.update(changes)Changes your own fields: a string sets one, null removes it, the rest stay. Resolves { custom }. Your own fields
ensureLoaded()Reads the standard fields and your own from the file, unless they’re loaded or loading. Resolves once they’re there. Read
refresh(), custom.refresh()Reads the standard fields, or your own, from the file again. Rarely needed: they stay up to date by themselves.
canUpdate()Whether changing the standard fields and your own ones is allowed. Permissions

Every method that returns a promise also takes a signal in its options, to cancel it.

State#

Every value is a signal on inject(EpdfMetadata): 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
fields()Every standard field, as listed under Read, or null until it has loadedgetSnapshot()
custom.fields()Your own fields, as { name: value }, or null until they have loadedcustom.getSnapshot()
status()'idle' until something asks for it, then 'loading', 'ready', 'forbidden' or 'error'getStatus(), or custom.getStatus() for your own fields

Events#

title-notice.ts
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { filter, map } from 'rxjs';
import { EpdfMetadata } from '@embedpdf/angular/metadata';

@Component({
  selector: 'app-title-notice',
  template: `<p role="status">{{ notice() }}</p>`,
})
export class TitleNotice {
  private readonly metadata = inject(EpdfMetadata);

  protected readonly notice = toSignal(
    this.metadata.updated$.pipe(
      filter(({ origin }) => origin.kind === 'remote'),
      map(({ metadata }) => `The title is now "${metadata.title}"`),
    ),
    { initialValue: '' },
  );
}
EventFires whenCarries
updated$The standard fields changed, by you or by someone elsemetadata, previous, changedKeys, origin
resynced$The standard fields were loaded from the file: the first time they’re asked for, after refresh(), or to catch up after a lost connectionmetadata
custom.updated$Your own fields changedcustom, previous, changedKeys, origin
custom.resynced$Your own fields were loaded from the filecustom

changedKeys names the fields that changed, and previous has the values before.

Permissions#

PermissionWhat it allowsCheck
doc.openReading every fieldnone: it opened
doc.metadata.modifyChanging the standard fields and your own onesmetadata.canUpdate()

Good to know#

The modified date#

Changing a property doesn’t change modifiedAt for you. Set it yourself when you want it to move: update({ modifiedAt: new Date() }).

Dates#

Dates are ISO strings, with the time zone the PDF gives, so they survive JSON. Write a date as an ISO string or a Date. See Metadata for the details.

Was this page helpful?

Your feedback goes directly to the documentation team.