Metadata
A PDF carries information about itself: a title, an author, when it was made. Register
metadataPlugin() to show it in your UI and let people change it:
import { useMetadata, useMetadataState } from '@embedpdf/react/metadata';
export function TitleField() {
const { update, canUpdate } = useMetadata();
const { metadata } = useMetadataState();
return (
<label>
Title
<input
defaultValue={metadata?.title ?? ''}
disabled={!canUpdate()}
onBlur={(event) => update({ title: event.target.value })}
/>
</label>
);
}What useMetadataState() 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 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: useMetadataState()
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 useMetadataState() gives you, and every status, is listed under State.
Loading live preview…
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { metadataPlugin, useMetadataState } from '@embedpdf/react/metadata';
import { localEngine } from '@embedpdf/engine';
import './properties.css';
const engine = localEngine();
const plugins = [metadataPlugin()];
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.
function Properties() {
const { metadata, status } = useMetadataState();
return (
<section className="panel">
<header className="panel-header">
<h3 className="panel-title">Document properties</h3>
<span className="status" data-status={status}>
{status}
</span>
</header>
<dl className="properties">
{FIELDS.map(([field, label]) => {
const value = metadata ? show(field, metadata[field]) : null;
return (
<div className="property" key={field}>
<dt>{label}</dt>
<dd className={value === null ? 'unset' : undefined}>{value ?? 'not set'}</dd>
</div>
);
})}
</dl>
</section>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<Properties />
</DocumentGate>
</Viewer>
);
}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 { useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { DocumentMetadata, OpenInput } from '@embedpdf/react/runtime';
import {
metadataPlugin,
useMetadata,
useMetadataEvent,
useMetadataState,
} from '@embedpdf/react/metadata';
import { localEngine } from '@embedpdf/engine';
import './edit.css';
const engine = localEngine();
const plugins = [metadataPlugin()];
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.
function PropertiesForm({ initial }: { initial: DocumentMetadata }) {
const metadata = useMetadata();
const modifiedAt = useMetadataState((state) => state.metadata?.modifiedAt ?? null);
const [draft, setDraft] = useState<Draft>({
title: initial.title ?? '',
author: initial.author ?? '',
subject: initial.subject ?? '',
keywords: initial.keywords ?? '',
});
const [changes, setChanges] = useState<string[]>([]);
// Every change to the standard fields, by you or anyone else.
useMetadataEvent(
(capability) => capability.onUpdated,
({ changedKeys, origin }) =>
setChanges((list) => [
`${changedKeys.join(', ')} changed ${origin.kind === 'local' ? 'here' : 'elsewhere'}`,
...list,
]),
);
const canUpdate = metadata.canUpdate();
// An empty field removes it: null, not ''.
const save = () =>
metadata.update({
title: draft.title.trim() || null,
author: draft.author.trim() || null,
subject: draft.subject.trim() || null,
keywords: draft.keywords.trim() || null,
});
return (
<div className="layout">
<form
className="form"
onSubmit={(event) => {
event.preventDefault();
void save();
}}
>
{EDITABLE.map((key) => (
<label className="row" key={key}>
<span className="name">{key}</span>
<input
className="field"
value={draft[key]}
disabled={!canUpdate}
placeholder="not set"
onChange={(event) => setDraft({ ...draft, [key]: event.target.value })}
/>
</label>
))}
<div className="actions">
<button type="submit" className="button primary" disabled={!canUpdate}>
Save
</button>
<button
type="button"
className="button"
disabled={!canUpdate}
onClick={() => metadata.update({ modifiedAt: new Date() })}
>
Set the modified date to now
</button>
</div>
</form>
<section className="changes" aria-live="polite">
<h3 className="changes-title">Changes</h3>
<p className="modified">
Last changed: {modifiedAt ? new Date(modifiedAt).toLocaleString() : 'not set'}
</p>
{changes.length === 0 ? (
<p className="empty">Edit a field and save.</p>
) : (
<ul className="log">
{changes.map((change, index) => (
<li key={changes.length - index}>{change}</li>
))}
</ul>
)}
</section>
</div>
);
}
// The form starts once the properties have been read from the file.
function PropertiesEditor() {
const fields = useMetadataState((state) => state.metadata);
return fields ? <PropertiesForm initial={fields} /> : <p className="loading">Reading…</p>;
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<PropertiesEditor />
</DocumentGate>
</Viewer>
);
}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 { useMetadataState } from '@embedpdf/react/metadata';
export function ContractNumber() {
const { custom } = useMetadataState(); // { contractId: 'C-2026-114', reviewedBy: 'dana' }
return <span>Contract {custom?.contractId}</span>;
}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 { useEffect, useState } from 'react';
import { Viewer, DocumentGate, isPluginError } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { metadataPlugin, useMetadata, useMetadataState } from '@embedpdf/react/metadata';
import { localEngine } from '@embedpdf/engine';
import './custom-fields.css';
const engine = localEngine();
const plugins = [metadataPlugin()];
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.
function CustomFields() {
const metadata = useMetadata();
const { custom } = useMetadataState();
const [name, setName] = useState('');
const [value, setValue] = useState('');
const [error, setError] = useState<string | null>(null);
// Two fields of our own, written on load. Setting the same values again changes nothing.
useEffect(() => {
void metadata.custom.update({ contractId: 'C-2026-114', reviewedBy: 'dana' });
}, [metadata]);
const canUpdate = metadata.canUpdate();
const add = async () => {
try {
await metadata.custom.update({ [name.trim()]: value });
setName('');
setValue('');
setError(null);
} catch (failure) {
// A name the PDF can't take, such as one of the standard fields.
if (isPluginError(failure, 'invalid-input')) setError(failure.message);
else throw failure;
}
};
return (
<section className="panel">
<table className="fields">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
<th aria-label="Remove" />
</tr>
</thead>
<tbody>
{Object.entries(custom ?? {}).map(([key, text]) => (
<tr key={key}>
<td className="key">{key}</td>
<td>{text}</td>
<td>
<button
type="button"
className="button quiet"
disabled={!canUpdate}
// null removes a field; the others stay as they are.
onClick={() => metadata.custom.update({ [key]: null })}
>
Remove
</button>
</td>
</tr>
))}
</tbody>
</table>
<form
className="add"
onSubmit={(event) => {
event.preventDefault();
void add();
}}
>
<input
className="field"
aria-label="Name"
placeholder="Name, such as approvedOn"
value={name}
onChange={(event) => setName(event.target.value)}
/>
<input
className="field"
aria-label="Value"
placeholder="Value"
value={value}
onChange={(event) => setValue(event.target.value)}
/>
<button type="submit" className="button" disabled={!canUpdate || !name.trim()}>
Add
</button>
</form>
{error && <p className="error">{error}</p>}
</section>
);
}
export default function App() {
return (
<Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate fallback={<p className="loading">Loading…</p>}>
<CustomFields />
</DocumentGate>
</Viewer>
);
}Methods#
Everything useMetadata() 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#
Everything useMetadataState() gives you. Your component re-renders when a value changes. The method in the last column reads the same value once.
| Field | What it is | Read once |
|---|---|---|
metadata | Every standard field, as listed under Read, or null until it has loaded | getSnapshot() |
custom | 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 { useState } from 'react';
import { useMetadataEvent } from '@embedpdf/react/metadata';
export function TitleNotice() {
const [notice, setNotice] = useState('');
useMetadataEvent(
(metadata) => metadata.onUpdated,
({ metadata, origin }) => {
if (origin.kind === 'remote') setNotice(`The title is now "${metadata.title}"`);
},
);
return <p role="status">{notice}</p>;
}| Event | Fires when | Carries |
|---|---|---|
onUpdated | The standard fields changed, by you or by someone else | metadata, previous, changedKeys, origin |
onResynced | 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.onUpdated | Your own fields changed | custom, previous, changedKeys, origin |
custom.onResynced | 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.