EmbedPDF

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:

title-field.svelte
<script lang="ts">
  import { useMetadata, useMetadataState } from '@embedpdf/svelte/metadata';

  const metadata = useMetadata();
  const state = useMetadataState();
</script>

<label>
  Title
  <input
    value={state.metadata?.title ?? ''}
    disabled={!metadata.canUpdate()}
    onblur={(event) => metadata.update({ title: event.currentTarget.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:

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: 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…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { metadataPlugin } from '@embedpdf/svelte/metadata';
  import { localEngine } from '@embedpdf/engine';
  import Properties from './Properties.svelte';

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

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <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…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { metadataPlugin } from '@embedpdf/svelte/metadata';
  import { localEngine } from '@embedpdf/engine';
  import PropertiesEditor from './PropertiesEditor.svelte';

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

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <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:

custom-fields.svelte
<script lang="ts">
  import { useMetadataState } from '@embedpdf/svelte/metadata';

  const state = useMetadataState(); // state.custom is { contractId: 'C-2026-114', reviewedBy: 'dana' }
</script>

<span>Contract {state.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…

<script lang="ts" module>
  import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
  import { metadataPlugin } from '@embedpdf/svelte/metadata';
  import { localEngine } from '@embedpdf/engine';
  import CustomFields from './CustomFields.svelte';

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

<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
  <DocumentGate>
    {#snippet fallback()}
      <p class="loading">Loading…</p>
    {/snippet}
    <CustomFields />
  </DocumentGate>
</Viewer>

Methods#

Everything useMetadata() 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#

Everything useMetadataState() gives you, as one reactive object: read its fields, and your markup updates when they change. The method in the last column reads the same value once.

FieldWhat it isRead once
metadataEvery standard field, as listed under Read, or null until it has loadedgetSnapshot()
customYour 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.svelte
<script lang="ts">
  import { useMetadataEvent } from '@embedpdf/svelte/metadata';

  let notice = $state('');

  useMetadataEvent(
    (metadata) => metadata.onUpdated,
    ({ metadata, origin }) => {
      if (origin.kind === 'remote') notice = `The title is now "${metadata.title}"`;
    },
  );
</script>

<p role="status">{notice}</p>
EventFires whenCarries
onUpdatedThe standard fields changed, by you or by someone elsemetadata, previous, changedKeys, origin
onResyncedThe 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.onUpdatedYour own fields changedcustom, previous, changedKeys, origin
custom.onResyncedYour 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.