---
title: "Metadata — Svelte"
description: "Show and change a document's properties: title, author, subject, keywords, dates, and your own fields."
framework: "Svelte"
source: "https://www.embedpdf.com/docs/headless/svelte/documents/metadata"
---

# 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`**

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

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

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

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

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

Every field `useMetadataState()` gives you, and every status, is listed under [State](#state).

**`App.svelte`**

```svelte
<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>
```

**`properties.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --ready: #1f8a4c;
  --ready-tint: #e8f6ee;
  --waiting: #a86a00;
  --waiting-tint: #fdf3e1;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The properties, in a card */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
  overflow: hidden;
}

.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  background: var(--canvas);
  border-bottom: 1px solid var(--line);
}

.panel-title {
  margin: 0;
  font-size: 14px;
  font-weight: 650;
  color: var(--ink);
}

/* The load state: loading, ready, forbidden or error */
.status {
  font-size: 11.5px;
  font-weight: 600;
  border-radius: 999px;
  padding: 3px 9px;
  color: var(--waiting);
  background: var(--waiting-tint);
}

.status[data-status='ready'] {
  color: var(--ready);
  background: var(--ready-tint);
}

/* One field per row: its name beside its value, stacked on a phone */
.properties {
  container-type: inline-size;
  margin: 0;
  padding: 4px 16px 10px;
}

.property {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2px;
  padding: 9px 0;
  border-bottom: 1px solid var(--line);
}

.property:last-child {
  border-bottom: 0;
}

@container (min-width: 460px) {
  .property {
    grid-template-columns: 180px 1fr;
    gap: 16px;
  }
}

.property dt {
  font-size: 13px;
  color: var(--muted);
}

.property dd {
  margin: 0;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.property dd.unset {
  font-weight: 400;
  font-style: italic;
  color: var(--muted);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`Properties.svelte`**

```svelte
<!-- The document's properties, the way a "Document properties" dialog shows them. -->
<script lang="ts">
  import { useMetadataState } from '@embedpdf/svelte/metadata';

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

  const properties = useMetadataState();
</script>

<section class="panel">
  <header class="panel-header">
    <h3 class="panel-title">Document properties</h3>
    <span class="status" data-status={properties.status}>
      {properties.status}
    </span>
  </header>
  <dl class="properties">
    {#each FIELDS as [field, label] (field)}
      {@const value = properties.metadata ? show(field, properties.metadata[field]) : null}
      <div class="property">
        <dt>{label}</dt>
        <dd class={value === null ? 'unset' : undefined}>{value ?? 'not set'}</dd>
      </div>
    {/each}
  </dl>
</section>
```

## Change

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

```ts
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.

**`App.svelte`**

```svelte
<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>
```

**`edit.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #0876fd;
  --accent-strong: #0565da;
  --on-accent: #ffffff;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.button.primary {
  color: var(--on-accent);
  background: var(--accent);
  border-color: var(--accent);
}

.button.primary:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}

/* The form and the changes: side by side where there's room, stacked on a phone */
.layout {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.form,
.changes {
  flex: 1 1 100%;
  box-sizing: border-box;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 14px 16px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

@container (min-width: 620px) {
  .form {
    flex: 1 1 0;
    min-width: 0;
  }

  .changes {
    flex: 0 0 240px;
  }
}

/* One field per row: its name above its input */
.row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 10px;
}

.name {
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}

.field {
  appearance: none;
  box-sizing: border-box;
  width: 100%;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 11px;
}

.field::placeholder {
  font-style: italic;
  color: var(--muted);
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 4px;
}

/* What changed, newest first */
.changes-title {
  margin: 0 0 6px;
  font-size: 13px;
  font-weight: 650;
  color: var(--ink);
}

.modified,
.empty {
  margin: 0 0 8px;
  font-size: 12.5px;
  color: var(--muted);
}

.log {
  margin: 0;
  padding: 0;
  list-style: none;
}

.log li {
  padding: 6px 0;
  font-size: 12.5px;
  color: var(--ink);
  border-top: 1px solid var(--line);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`PropertiesEditor.svelte`**

```svelte
<!-- The form starts once the properties have been read from the file. -->
<script lang="ts">
  import { useMetadataState } from '@embedpdf/svelte/metadata';
  import PropertiesForm from './PropertiesForm.svelte';

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

{#if fields.current}
  <PropertiesForm initial={fields.current} />
{:else}
  <p class="loading">Reading…</p>
{/if}
```

**`PropertiesForm.svelte`**

```svelte
<!-- A properties form: the inputs start with what the file says, and Save writes them back. -->
<script lang="ts">
  import { untrack } from 'svelte';
  import type { DocumentMetadata } from '@embedpdf/svelte/runtime';
  import { useMetadata, useMetadataEvent, useMetadataState } from '@embedpdf/svelte/metadata';

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

  let { initial }: { initial: DocumentMetadata } = $props();

  const metadata = useMetadata();
  const modifiedAt = useMetadataState((fields) => fields.metadata?.modifiedAt ?? null);
  // What the file said when the form opened; the inputs edit it from there.
  const draft = $state<Draft>(
    untrack(() => ({
      title: initial.title ?? '',
      author: initial.author ?? '',
      subject: initial.subject ?? '',
      keywords: initial.keywords ?? '',
    })),
  );
  let changes = $state<string[]>([]);

  // Every change to the standard fields, by you or anyone else.
  useMetadataEvent(
    (capability) => capability.onUpdated,
    ({ changedKeys, origin }) => {
      changes = [
        `${changedKeys.join(', ')} changed ${origin.kind === 'local' ? 'here' : 'elsewhere'}`,
        ...changes,
      ];
    },
  );

  const canUpdate = $derived(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,
    });
</script>

<div class="layout">
  <form
    class="form"
    onsubmit={(event) => {
      event.preventDefault();
      void save();
    }}
  >
    {#each EDITABLE as key (key)}
      <label class="row">
        <span class="name">{key}</span>
        <input class="field" bind:value={draft[key]} disabled={!canUpdate} placeholder="not set" />
      </label>
    {/each}
    <div class="actions">
      <button type="submit" class="button primary" disabled={!canUpdate}>Save</button>
      <button
        type="button"
        class="button"
        disabled={!canUpdate}
        onclick={() => metadata.update({ modifiedAt: new Date() })}
      >
        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.current ? new Date(modifiedAt.current).toLocaleString() : 'not set'}
    </p>
    {#if changes.length === 0}
      <p class="empty">Edit a field and save.</p>
    {:else}
      <ul class="log">
        {#each changes as change, index (changes.length - index)}
          <li>{change}</li>
        {/each}
      </ul>
    {/if}
  </section>
</div>
```

## 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`**

```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>
```

```ts
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.

**`App.svelte`**

```svelte
<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>
```

**`custom-fields.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --danger: #c62a2a;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The fields, in a card */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
  overflow: hidden;
}

.fields {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.fields th {
  padding: 10px 16px;
  font-size: 12px;
  font-weight: 600;
  text-align: left;
  color: var(--muted);
  background: var(--canvas);
  border-bottom: 1px solid var(--line);
}

.fields td {
  padding: 8px 16px;
  color: var(--ink);
  border-bottom: 1px solid var(--line);
  overflow-wrap: anywhere;
}

.fields td:last-child {
  width: 1%;
  text-align: right;
}

.key {
  font-family: ui-monospace, monospace;
  font-size: 12.5px;
}

/* A new field: name, value and the button, wrapping on a phone */
.add {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 12px 16px;
}

.field {
  appearance: none;
  flex: 1 1 150px;
  min-width: 0;
  box-sizing: border-box;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 11px;
}

.field::placeholder {
  color: var(--muted);
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 14px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.button.quiet {
  padding: 6px 10px;
  font-size: 12px;
  color: var(--danger);
  box-shadow: none;
}

.error {
  margin: 0;
  padding: 0 16px 12px;
  font-size: 12.5px;
  color: var(--danger);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`CustomFields.svelte`**

```svelte
<!-- Your own fields, kept in the document: add one, change one, remove one. -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { isPluginError } from '@embedpdf/svelte/runtime';
  import { useMetadata, useMetadataState } from '@embedpdf/svelte/metadata';

  const metadata = useMetadata();
  const fields = useMetadataState();
  let name = $state('');
  let value = $state('');
  let error = $state<string | null>(null);

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

  const canUpdate = $derived(metadata.canUpdate());

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

<section class="panel">
  <table class="fields">
    <thead>
      <tr>
        <th>Name</th>
        <th>Value</th>
        <th aria-label="Remove"></th>
      </tr>
    </thead>
    <tbody>
      {#each Object.entries(fields.custom ?? {}) as [key, text] (key)}
        <tr>
          <td class="key">{key}</td>
          <td>{text}</td>
          <td>
            <!-- null removes a field; the others stay as they are. -->
            <button
              type="button"
              class="button quiet"
              disabled={!canUpdate}
              onclick={() => metadata.custom.update({ [key]: null })}
            >
              Remove
            </button>
          </td>
        </tr>
      {/each}
    </tbody>
  </table>
  <form
    class="add"
    onsubmit={(event) => {
      event.preventDefault();
      void add();
    }}
  >
    <input
      class="field"
      aria-label="Name"
      placeholder="Name, such as approvedOn"
      bind:value={name}
    />
    <input class="field" aria-label="Value" placeholder="Value" bind:value />
    <button type="submit" class="button" disabled={!canUpdate || !name.trim()}>Add</button>
  </form>
  {#if error}
    <p class="error">{error}</p>
  {/if}
</section>
```

## 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](#change)        |
| `custom.update(changes)`        | Changes your own fields: a string sets one, `null` removes it, the rest stay. Resolves `{ custom }`. [Your own fields](#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](#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](#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.

| Field      | What it is                                                                                    | Read once                                                  |
| :--------- | :-------------------------------------------------------------------------------------------- | :--------------------------------------------------------- |
| `metadata` | Every standard field, as listed under [Read](#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

**`title-notice.svelte`**

```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>
```

| 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](https://www.embedpdf.com/docs/engine/documents/metadata) for the details.
