---
title: "Stamps — React"
description: "Libraries of reusable stamps — Acrobat-compatible PDFs — placed by click or by code, made from selections, kept across sessions."
framework: "React"
source: "https://www.embedpdf.com/docs/headless/react/plugins/stamp"
---

# Stamps

A stamp is a piece of artwork you place on a page as a stamp annotation —
"Approved", a signature, a company mark. The stamp plugin keeps those in
*libraries*, hands one to the annotation plugin when you arm it, and writes
the placed annotation the way Acrobat does, so a stamp placed here reads as
the same stamp there.

Register `stampPlugin()` beside `annotationPlugin()` and read the capability
with `useStamp()`. The example imports the standard library, lists its
stamps, and arms one — hover a page to see the ghost, click to place:

**`basic.tsx`**

```tsx
import { useEffect, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin, useTool } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/react/annotation';
import {
  stampPlugin,
  useArmStampAsset,
  useStamp,
  useStampAssetPreviewUrl,
  useStampAssets,
  useStampLibraries,
} from '@embedpdf/react/stamp';
import type { StampAsset } from '@embedpdf/react/stamp';
import { loadDefaultLibrary } from '@embedpdf/default-stamps/library';
import { localEngine } from '@embedpdf/engine';

import {
  Button,
  Demo,
  Readout,
  Spacer,
  StageFrame,
  Toolbar,
  stageFill,
} from '../stage/_shared/chrome';

const engine = localEngine();
const assetEngine = engine; // stamp libraries are PDFs; they open here too
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  stampPlugin({ assetEngine }),
];

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

/** One library, imported once: the standard stamps, English edition, loaded
 *  as a lazy chunk of this build. The file names itself (its /Title) and
 *  lists its stamps (its named pages). */
function useStandardStamps() {
  const stamp = useStamp();
  const libraries = useStampLibraries();
  const [error, setError] = useState<string | null>(null);
  useEffect(() => {
    if (libraries.length > 0) return;
    loadDefaultLibrary('en')
      .then((bytes) => stamp.importLibraryPdf(bytes))
      .catch((err) => setError(err instanceof Error ? err.message : String(err)));
    // Import once per workspace; the library list changing is the outcome.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [stamp]);
  return { libraries, error };
}

function StampCell({
  asset,
  armed,
  onArm,
}: {
  asset: StampAsset;
  armed: boolean;
  onArm: () => void;
}) {
  const url = useStampAssetPreviewUrl(asset.id);
  return (
    <Button title={`${asset.label} (/Name ${asset.name})`} onClick={onArm}>
      {armed ? '▸ ' : ''}
      {url ? <img src={url} alt={asset.label} style={{ height: 22 }} /> : asset.label}
    </Button>
  );
}

function StampPicker() {
  const { libraries, error } = useStandardStamps();
  const assets = useStampAssets();
  const { armAsset, disarm } = useArmStampAsset();
  const { activeToolId } = useTool();
  const [armedId, setArmedId] = useState<string | null>(null);
  // Leaving the stamp tool (Escape, another tool) un-highlights the picker.
  const armed = activeToolId === 'stamp' ? armedId : null;

  if (error) return <Readout>Could not load the stamps: {error}</Readout>;
  if (libraries.length === 0) return <Readout>Loading stamps…</Readout>;
  return (
    <Toolbar>
      <Readout>{libraries[0].name}</Readout>
      {assets.slice(0, 5).map((asset) => (
        <StampCell
          key={asset.id}
          asset={asset}
          armed={armed === asset.id}
          onArm={() => {
            setArmedId(asset.id);
            void armAsset(asset.id);
          }}
        />
      ))}
      <Spacer />
      <Button title="Put the stamp tool down" disabled={!armed} onClick={disarm}>
        Done
      </Button>
    </Toolbar>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <Demo>
        <DocumentGate fallback={<p>Loading…</p>}>
          <StampPicker />
          <StageFrame height={420}>
            <Stage style={stageFill}>
              {() => (
                <>
                  <RenderLayer annotations={false} />
                  <AnnotationLayer />
                </>
              )}
            </Stage>
          </StageFrame>
        </DocumentGate>
      </Demo>
    </Viewer>
  );
}
```

Two things the example shows beyond the calls themselves:

- **The library names itself.** `importLibraryPdf(bytes)` takes no name, no
  list of stamps, no manifest. The PDF's title is the library's name and its
  named pages are the stamps; an Acrobat-authored library imports as is.
- **The ghost stays sharp.** A vector stamp is rendered for the size it is
  shown at, and re-rendered when the zoom crosses a size step, so a large
  stamp previewed at 300% is as crisp as the placed one.

## A library is a PDF

Every library is one PDF, in the dialect Acrobat uses for its own stamp
files:

- the document `/Title` is the library name;
- the `/Names /Pages` registry lists the stamps as `identifier=label` pairs
  (`Approved=Goedgekeurd`) — one entry per page;
- every page is one stamp's artwork, vector or raster.

The identifier is the stamp's durable identity and becomes the placed
annotation's `/Name`; the label is what a picker shows and becomes the
placed `/Subj`. Two libraries can share identifiers (a Dutch and an English
"Approved" are the same stamp with different labels), which is how the
localized default libraries work.

That one shape gives you the rest for free:

```ts
const stamp = useStamp();

// Any PDF becomes a library. A plain PDF: one stamp per page, named Stamp1….
const libraryId = await stamp.importLibraryPdf(bytes);

// The library as the file it is — title, registry, artwork.
const pdf = stamp.exportLibrary(libraryId); // drop it into Acrobat's Stamps folder

// Add a stamp: a single-page PDF (vector) or a PNG/JPEG (becomes a page).
await stamp.addAsset({ libraryId, label: 'Paid', source: pngBytes });

// Relabel; the identifier never changes.
await stamp.updateAsset(assetId, { label: 'PAID' });
```

There is no manifest because there is nothing left for one to say. What has
no standard home in a PDF — a stable library id, a locale, a stamp's kind —
rides `/PieceInfo`, invisible to every other reader.

## Library kinds

A library says what it holds: `kind` is `'stamps'` (the default), `'signatures'`
(a person's marks — see [Signatures](https://www.embedpdf.com/docs/headless/react/plugins/signature)), or a
name of your own. It rides `/PieceInfo` with the library id, and a picker asks
for the kinds it lists:

```ts
await stamp.createLibrary('Acme review marks', { kind: 'toolbar' });
await stamp.importLibraryPdf(bytes, { libraryKind: 'legal-seals' }); // override what the file says

stamp.libraries({ kind: 'stamps' }); // one kind
useStampLibraries({ kind: ['stamps', 'legal-seals'] }); // several
```

## Author a mark

An asset can be **drawn, typed, an image, or a PDF page** — one `mark` in
place of `source`. Ink and text are rendered by the engine into a page of
the library (a vector appearance, exactly what a placed annotation would
show); an image becomes a page carrying it; a PDF page is extracted as is:

```ts
await stamp.addAsset({
  libraryId,
  label: 'Signature',
  mark: { kind: 'ink', strokes, strokeWidth: 2.5 },
});
await stamp.addAsset({
  libraryId,
  label: 'Initials',
  mark: { kind: 'text', text: 'AL', fontFamily: 'times-italic' },
});
await stamp.addAsset({ libraryId, label: 'Logo', mark: { kind: 'image', source: pngBytes } });
await stamp.addAsset({
  libraryId,
  label: 'Seal',
  mark: { kind: 'pdf', source: pdfBytes, pageIndex: 2 },
});

await stamp.updateLibrary(libraryId, { name: 'Ada Lovelace' }); // rename (/Title)
stamp.armedAsset(documentId); // the asset armed on a document, while the tool holds it
```

`fontFamily` is a standard PDF font name or the key of a font registered
through `engine.fonts` on the asset engine — a script face for typed
signatures, for instance.

## Place a stamp with code

A click after `armAsset` and a call to `placeAsset` produce the same
annotation from the same inputs — one placement law, two entry points:

**`programmatic.tsx`**

```tsx
import { useEffect, useState } from 'react';
import { Viewer, DocumentGate, useDocumentId } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, usePageList, usePages } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/react/annotation';
import { stampPlugin, useStamp, useStampAssets } from '@embedpdf/react/stamp';
import { loadDefaultLibrary } from '@embedpdf/default-stamps/library';
import { localEngine } from '@embedpdf/engine';

import {
  Button,
  Demo,
  Readout,
  Spacer,
  StageFrame,
  Toolbar,
  stageFill,
} from '../stage/_shared/chrome';

const engine = localEngine();
const assetEngine = engine; // stamp libraries are PDFs; they open here too
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  stampPlugin({ assetEngine }),
];

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

function PlaceByCode() {
  const stamp = useStamp();
  const assets = useStampAssets();
  const documentId = useDocumentId();
  const { currentPage } = usePages();
  const { pages } = usePageList();
  const page = pages[currentPage];
  const [status, setStatus] = useState('');

  useEffect(() => {
    if (assets.length > 0) return;
    loadDefaultLibrary('en')
      .then((bytes) => stamp.importLibraryPdf(bytes))
      .catch((err) => setStatus(err instanceof Error ? err.message : String(err)));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [stamp]);

  // The same box a click would produce: centred on `at` (page points, origin
  // top-left), fitted and clamped to the page, /Name and /Subj written.
  const place = async (identifier: string, at: { x: number; y: number }, rotation = 0) => {
    const asset = assets.find((a) => a.name === identifier);
    if (!asset || !documentId || !page) return;
    const ref = await stamp.placeAsset(documentId, asset.id, {
      pageObjectNumber: page.pon,
      at,
      targetWidth: 160,
      rotation,
    });
    setStatus(
      `placed ${asset.label} on page ${ref.pageObjectNumber === page.pon ? currentPage + 1 : '?'}`,
    );
  };

  return (
    <Toolbar>
      <Readout>page {currentPage + 1}</Readout>
      <Button
        title="Place the Approved stamp near the top-left corner of this page"
        disabled={assets.length === 0}
        onClick={() => void place('Approved', { x: 120, y: 90 })}
      >
        Approve
      </Button>
      <Button
        title="Place the Draft stamp, rotated"
        disabled={assets.length === 0}
        onClick={() => void place('Draft', { x: 300, y: 200 }, 15)}
      >
        Mark as draft
      </Button>
      <Spacer />
      <Readout>{status}</Readout>
    </Toolbar>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <Demo>
        <DocumentGate fallback={<p>Loading…</p>}>
          <PlaceByCode />
          <StageFrame height={420}>
            <Stage style={stageFill}>
              {() => (
                <>
                  <RenderLayer annotations={false} />
                  <AnnotationLayer />
                </>
              )}
            </Stage>
          </StageFrame>
        </DocumentGate>
      </Demo>
    </Viewer>
  );
}
```

```ts
const ref = await stamp.placeAsset(documentId, assetId, {
  pageObjectNumber: pon,
  at: { x: 120, y: 90 }, // page points, origin top-left: the box is centred here
  targetWidth: 160, // optional; default the stamp's own size
  rotation: 0, // optional, degrees clockwise
});
```

The box is fitted to the stamp's aspect and clamped to the page, the
`/Name` and `/Subj` are written, and the new annotation is selected, exactly
as a click would leave it. `ref` is the created annotation.

## Make a stamp from a selection

Select one or more annotations on a page and turn them into a stamp. The
engine exports their appearances as one single-page PDF sized to their
union — vector, positions preserved, exactly what the page shows — and the
plugin files it as a page of the library you name:

**`from-selection.tsx`**

```tsx
import { useState } from 'react';
import { Viewer, DocumentGate, useDocumentId } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin, useTool } from '@embedpdf/react/interaction';
import {
  AnnotationLayer,
  annotationPlugin,
  useAnnotation,
  useAnnotationSelection,
} from '@embedpdf/react/annotation';
import {
  stampPlugin,
  useArmStampAsset,
  useStamp,
  useStampAssetPreviewUrl,
  useStampAssets,
} from '@embedpdf/react/stamp';
import type { StampAsset } from '@embedpdf/react/stamp';
import { localEngine } from '@embedpdf/engine';

import {
  Button,
  Demo,
  Readout,
  Spacer,
  StageFrame,
  Toolbar,
  stageFill,
} from '../stage/_shared/chrome';

const engine = localEngine();
const assetEngine = engine; // stamp libraries are PDFs; they open here too
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  stampPlugin({ assetEngine }),
];

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 MY_STAMPS = 'my-stamps';

function MyStamp({ asset, onArm }: { asset: StampAsset; onArm: () => void }) {
  const url = useStampAssetPreviewUrl(asset.id);
  return (
    <Button title={`Place "${asset.label}"`} onClick={onArm}>
      {url ? <img src={url} alt={asset.label} style={{ height: 22 }} /> : asset.label}
    </Button>
  );
}

function MakeStamp() {
  const stamp = useStamp();
  const documentId = useDocumentId();
  const annotation = useAnnotation();
  // Subscribed to the selection (ids) so this re-renders as it changes; the
  // refs come from the selected DTOs.
  const selectedIds = useAnnotationSelection();
  const selection = selectedIds.length > 0 ? annotation.getSelected().map((dto) => dto.ref) : [];
  const mine = useStampAssets(MY_STAMPS);
  const { armAsset } = useArmStampAsset();
  const { activeToolId, activate } = useTool();
  const [status, setStatus] = useState('draw a shape, select it, make a stamp');

  // One page at a time: a stamp is one page of artwork.
  const pages = new Set(selection.map((ref) => ref.pageObjectNumber));
  const canMake = selection.length > 0 && pages.size === 1 && documentId !== null;

  const make = async () => {
    if (!canMake || !documentId) return;
    const [pon] = pages;
    // The library is created on first use; the identifier is minted in
    // Acrobat's `#…` form so the stamp keeps its identity there too.
    const libraryId = stamp.library(MY_STAMPS)
      ? MY_STAMPS
      : await stamp.createLibrary('My stamps', { id: MY_STAMPS });
    const assetId = await stamp.addAssetFromAnnotations(documentId, pon, [...selection], {
      libraryId,
      label: `Custom stamp ${mine.length + 1}`,
    });
    setStatus(`added ${stamp.asset(assetId)?.label ?? assetId}`);
  };

  return (
    <Toolbar>
      <Button
        title="Draw a rectangle on the page"
        onClick={() => activate(activeToolId === 'square' ? 'pointer' : 'square')}
      >
        {activeToolId === 'square' ? '▸ ' : ''}▭ Draw
      </Button>
      <Button
        title="Turn the selected annotation(s) into a reusable stamp"
        disabled={!canMake}
        onClick={() => void make().catch((err) => setStatus(String(err)))}
      >
        Make stamp
      </Button>
      {mine.map((asset) => (
        <MyStamp key={asset.id} asset={asset} onArm={() => void armAsset(asset.id)} />
      ))}
      <Spacer />
      <Readout>{status}</Readout>
    </Toolbar>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <Demo>
        <DocumentGate fallback={<p>Loading…</p>}>
          <MakeStamp />
          <StageFrame height={420}>
            <Stage style={stageFill}>
              {() => (
                <>
                  <RenderLayer annotations={false} />
                  <AnnotationLayer />
                </>
              )}
            </Stage>
          </StageFrame>
        </DocumentGate>
      </Demo>
    </Viewer>
  );
}
```

```ts
const assetId = await stamp.addAssetFromAnnotations(documentId, pon, refs, {
  libraryId, // omit to create a library named after the label
  label: 'Custom stamp 1',
});
```

The identifier is minted in Acrobat's own form (`#` plus 22 characters), so
a stamp made here keeps its identity when its library is opened in Acrobat.
The call is all-or-nothing: a hidden annotation, one without an appearance,
or one on another page rejects the whole thing — a stamp silently missing a
part would be worse than an error.

## Keep custom libraries

The plugin knows *when* a library changes and *what* its bytes are; where
they live is your decision. Two calls are all a store needs:

```ts
stamp.onLibraryChanged(({ libraryId, reason }) => {
  /* 'created' | 'imported' | 'asset-added' | 'asset-updated' | 'asset-removed' | 'removed' */
});
stamp.exportLibrary(libraryId); // the complete PDF
```

`restoreStampLibraries` and `persistStampLibraries` wire those to a
`StampLibraryStore` — `list`, `put`, `delete` over bytes by library id.
`indexedDbByteStore` is the browser default; an in-memory store serves
tests; your own backend implements the three calls once:

**`persist.tsx`**

```tsx
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/react/annotation';
import {
  indexedDbByteStore,
  persistStampLibraries,
  restoreStampLibraries,
  stampPlugin,
  useArmStampAsset,
  useStamp,
  useStampAssets,
  useStampLibraries,
} from '@embedpdf/react/stamp';
import { localEngine } from '@embedpdf/engine';

import {
  Button,
  Demo,
  Readout,
  Spacer,
  StageFrame,
  Toolbar,
  stageFill,
} from '../stage/_shared/chrome';

const engine = localEngine();
const assetEngine = engine; // stamp libraries are PDFs; they open here too
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  annotationPlugin(),
  stampPlugin({ assetEngine }),
];

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

/** Where the library PDFs live between sessions: one IndexedDB store. Any
 *  object with `list` / `put` / `delete` works — a backend of your own too. */
const store = indexedDbByteStore('stamp-docs-demo');

function Libraries() {
  const stamp = useStamp();
  const libraries = useStampLibraries();
  const assets = useStampAssets();
  const { armAsset } = useArmStampAsset();
  const fileRef = useRef<HTMLInputElement>(null);
  const [restored, setRestored] = useState<number | null>(null);

  // Restore on mount; from then on every change writes the library back.
  useEffect(() => {
    void restoreStampLibraries(stamp, store).then((ids) => setRestored(ids.length));
    return persistStampLibraries(stamp, store);
  }, [stamp]);

  const importPdf = (file: File) =>
    // The file name is a fallback: a library names itself through its /Title.
    void stamp.importLibraryPdf(file, { name: file.name.replace(/\.pdf$/i, '') });

  const exportPdf = (libraryId: string, name: string) => {
    const bytes = stamp.exportLibrary(libraryId);
    if (!bytes) return;
    const url = URL.createObjectURL(new Blob([bytes as BlobPart], { type: 'application/pdf' }));
    Object.assign(document.createElement('a'), { href: url, download: `${name}.pdf` }).click();
    URL.revokeObjectURL(url);
  };

  return (
    <Toolbar>
      <Readout>
        {restored === null
          ? 'restoring…'
          : `${restored} restored · reload the page to see them come back`}
      </Readout>
      <Spacer />
      {libraries.map((library) => (
        <Button
          key={library.id}
          title={`Download "${library.name}" as a PDF — open it in Acrobat, or import it here again`}
          onClick={() => exportPdf(library.id, library.name)}
        >
          ⬇ {library.name}
        </Button>
      ))}
      {assets.slice(0, 3).map((asset) => (
        <Button
          key={asset.id}
          title={`Place "${asset.label}"`}
          onClick={() => void armAsset(asset.id)}
        >
          {asset.label}
        </Button>
      ))}
      <Button
        title="Import any PDF as a library: one stamp per page"
        onClick={() => fileRef.current?.click()}
      >
        + Import PDF
      </Button>
      <input
        ref={fileRef}
        type="file"
        accept="application/pdf"
        hidden
        onChange={(event) => {
          const file = event.currentTarget.files?.[0];
          event.currentTarget.value = '';
          if (file) importPdf(file);
        }}
      />
    </Toolbar>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <Demo>
        <DocumentGate fallback={<p>Loading…</p>}>
          <Libraries />
          <StageFrame height={420}>
            <Stage style={stageFill}>
              {() => (
                <>
                  <RenderLayer annotations={false} />
                  <AnnotationLayer />
                </>
              )}
            </Stage>
          </StageFrame>
        </DocumentGate>
      </Demo>
    </Viewer>
  );
}
```

```ts
useEffect(() => {
  void restoreStampLibraries(stamp, store); // import every stored PDF
  return persistStampLibraries(stamp, store, {
    except: ['embedpdf-standard'], // libraries you fetch fresh each time
  });
}, [stamp]);
```

Writes are coalesced per library, so a burst of edits saves once, and a
pending write is flushed when you unsubscribe.

## Dynamic stamps

A stamp PDF can carry form fields — the name, the date, a document title.
When the actions plugin's JavaScript switch is on, such a *template* is
evaluated at placement in a detached script realm with the viewer's
identity and clock, then flattened into the placed artwork; the library
page itself is never changed. With scripting off, or without the actions
plugin, the template is placed as it is.

```ts
stampPlugin({ dynamic: false }); // keep templates static even with scripting on
```

`dynamic` is a product choice, not a trust boundary — the actions plugin's
policy is the one that decides whether scripts run at all.

## Localized default stamps

`@embedpdf/default-stamps` ships the standard set as one library per locale
(`en`, `de`, `nl`, `fr`, `es`, `zh-CN`, `sv`, `ja`), each registering the same
identifiers with translated labels. Its `library` entry delivers each locale
as a lazy module of your own build — no asset to copy, no CDN, nothing
fetched from anywhere but your origin:

```ts
import { LOCALES, loadDefaultLibrary } from '@embedpdf/default-stamps/library';
import { negotiateLocale } from '@embedpdf/react/i18n';

const locale = negotiateLocale(LOCALES, navigator.languages) ?? 'en';
await stamp.importLibraryPdf(await loadDefaultLibrary(locale)); // "Standaard stempels", id embedpdf-standard
```

Import two locales and you get two libraries with one identity: the
placed `/Name` is `Approved` either way, only the `/Subj` differs. The PDFs
themselves stay in the package (`<locale>/stamps.pdf`) for self-hosting and
for Acrobat.

## Permissions

Placement writes an annotation, so it needs `doc.annotate.modify` on the
document. Making a stamp from a selection reads appearances out of the
document — that egresses content and is gated by `doc.download`. Library
operations run on the asset engine and touch no document of yours.
