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:
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:
| Field | What it is |
|---|---|
title | The document’s title |
author | Who wrote it |
subject | What it’s about |
keywords | Keywords, as one string (often separated by commas) |
creator | The app the document was made in |
producer | The app that turned it into a PDF |
createdAt | When it was created, as an ISO date |
modifiedAt | When it was last changed, as an ISO date |
trapped | A 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:
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.
| Method | What 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.
| Field | What it is | Read once |
|---|---|---|
fields() | Every standard field, as listed under Read, or null until it has loaded | getSnapshot() |
custom.fields() | Your own fields, as { name: value }, or null until they have loaded | custom.getSnapshot() |
status() | 'idle' until something asks for it, then 'loading', 'ready', 'forbidden' or 'error' | getStatus(), or custom.getStatus() for your own fields |
Events#
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: '' },
);
}| Event | Fires when | Carries |
|---|---|---|
updated$ | The standard fields changed, by you or by someone else | metadata, 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 connection | metadata |
custom.updated$ | Your own fields changed | custom, previous, changedKeys, origin |
custom.resynced$ | Your own fields were loaded from the file | custom |
changedKeys names the fields that changed, and previous has the values before.
Permissions#
| Permission | What it allows | Check |
|---|---|---|
doc.open | Reading every field | none: it opened |
doc.metadata.modify | Changing the standard fields and your own ones | metadata.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.
Your feedback goes directly to the documentation team.