EmbedPDF

Stamps

A stamp is artwork you place on a page: “Approved”, a company seal, a signature. The stamp plugin keeps stamps in libraries, arms one for a click, and writes the placed stamp the way Acrobat does, so it’s the same stamp there.

Register stampPlugin() next to annotationPlugin(), and use useStamp(). The example imports the standard library, lists its stamps and arms one: hover a page to see where it lands, and click to place it:

Loading live preview…

import { useEffect, useRef } 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 {
  stampPlugin,
  useStamp,
  useStampAssetPreviewUrl,
  useStampAssets,
  useStampLibraries,
  useStampState,
} from '@embedpdf/react/stamp';
import type { StampAsset } from '@embedpdf/react/stamp';
import { loadDefaultLibrary } from '@embedpdf/default-stamps/library';
import { localEngine } from '@embedpdf/engine';

import './basic.css';

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 StampButton({ asset, armed }: { asset: StampAsset; armed: boolean }) {
  const stamp = useStamp();
  const url = useStampAssetPreviewUrl(asset.id);

  return (
    <button
      type="button"
      className="button"
      title={asset.label}
      aria-pressed={armed}
      onClick={() => (armed ? stamp.disarm() : void stamp.armAsset(asset.id))}
    >
      {url ? <img src={url} alt={asset.label} className="preview" /> : asset.label}
    </button>
  );
}

function StampPicker() {
  const stamp = useStamp();
  const [library] = useStampLibraries();
  const assets = useStampAssets({ libraryId: library?.id });
  const { armedAsset } = useStampState(); // the stamp the next click places
  const imported = useRef(false);

  // On load: the standard stamps, English edition, with "Approved" armed.
  useEffect(() => {
    if (imported.current) return;
    imported.current = true;
    void loadDefaultLibrary('en')
      .then((bytes) => stamp.importLibrary(bytes))
      .then(({ library }) => {
        const approved = stamp
          .listAssets({ libraryId: library.id })
          .find((asset) => asset.name === 'Approved');
        if (approved) return stamp.armAsset(approved.id);
      });
  }, [stamp]);

  if (!library) {
    return (
      <div className="toolbar">
        <output className="readout">Loading the stamps…</output>
      </div>
    );
  }

  return (
    <div className="toolbar">
      {assets.slice(0, 6).map((asset) => (
        <StampButton key={asset.id} asset={asset} armed={armedAsset?.id === asset.id} />
      ))}
      <span className="spacer" />
      <output className="readout">
        {armedAsset ? `Click a page to place “${armedAsset.label}”` : 'Pick a stamp'}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <StampPicker />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
const { library } = await stamp.importLibrary(await loadDefaultLibrary('en'));
await stamp.armAsset(assetId); // the next click places it
  • A library names itself. A library is a PDF: its title is the library’s name, and its named pages are the stamps. An Acrobat stamp file imports as it is.
  • Libraries are shared, placing is per document. Every open document sees the same libraries. Arming and placing act on the document your component is in, like every other hook.
  • The preview stays sharp. A vector stamp is drawn for the size it’s shown at, so a big stamp at 300% is as crisp as the placed one.

A library is a PDF#

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

  • the document’s title is the library’s name;
  • the named pages are the stamps, as identifier=label pairs such as Approved=Goedgekeurd;
  • each page is one stamp’s artwork, vector or image.

The identifier is the stamp’s lasting identity, and the placed annotation’s name. The label is what a picker shows. Two libraries can share identifiers: a Dutch and an English “Approved” are the same stamp with different labels.

// Any PDF becomes a library. A plain PDF gives one stamp per page.
const { library } = await stamp.importLibrary(bytes);
 
// The library as the PDF it is: drop it into Acrobat's stamps folder
const pdf = await stamp.exportLibrary(library.id);
 
// Add a stamp: a one-page PDF (vector) or a PNG or JPEG
const { asset } = await stamp.createAsset({ libraryId: library.id, label: 'Paid', source: pngBytes });
 
// Rename it; the identifier never changes
await stamp.updateAsset(asset.id, { label: 'PAID' });

Show a stamp picker#

A button per stamp arms it, and each button shows the stamp’s preview:

picker.tsx
import { useStamp, useStampAssets } from '@embedpdf/react/stamp';

import { StampPreview } from './preview';

export function StampPicker({ libraryId }: { libraryId: string }) {
  const stamp = useStamp();
  const assets = useStampAssets({ libraryId });

  return assets.map((asset) => (
    <button key={asset.id} onClick={() => stamp.armAsset(asset.id)}>
      <StampPreview assetId={asset.id} />
      {asset.label}
    </button>
  ));
}
preview.tsx
import { useStampAssetPreviewUrl } from '@embedpdf/react/stamp';

export function StampPreview({ assetId }: { assetId: string }) {
  const url = useStampAssetPreviewUrl(assetId);
  return url ? <img src={url} alt="" /> : null;
}

useStampLibraries() lists the libraries, and useStampAssets({ libraryId }) the stamps in one. Both update when stamps are added, renamed or removed. useStampState().armedAsset is the armed stamp, to show which button is on.

Library kinds#

A library says what it holds: kind is 'stamps' (the default), 'signatures' (a person’s marks; see Signatures), or a name of your own:

await stamp.createLibrary('Review marks', { kind: 'review' });
await stamp.importLibrary(bytes, { libraryKind: 'legal-seals' }); // override what the file says

A picker asks for the kinds it shows:

library-kinds.tsx
import { useStampLibraries } from '@embedpdf/react/stamp';

export function LibraryList() {
  const libraries = useStampLibraries({ kind: ['stamps', 'legal-seals'] });

  return (
    <ul>
      {libraries.map((library) => (
        <li key={library.id}>{library.name}</li>
      ))}
    </ul>
  );
}

Draw or type a stamp#

A stamp can be drawn, typed, an image, or a page of a PDF. Pass a mark instead of source:

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

Ink and text are drawn by the engine as vector artwork, exactly as a placed annotation would show. The example makes a drawn signature and typed initials; type more, and place them:

Loading live preview…

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 {
  stampPlugin,
  useStamp,
  useStampAssetPreviewUrl,
  useStampAssets,
  useStampLibraries,
  useStampState,
} from '@embedpdf/react/stamp';
import type { StampAsset } from '@embedpdf/react/stamp';
import { localEngine } from '@embedpdf/engine';

import './marks.css';

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

// A signature as a pen would draw it: two strokes, in points.
const loop = Array.from({ length: 48 }, (_, i) => ({
  x: i * 4,
  y: 30 - Math.sin(i / 3) * 16 - i * 0.2,
}));
const underline = [
  { x: 10, y: 52 },
  { x: 180, y: 46 },
];

function MarkButton({ asset, armed }: { asset: StampAsset; armed: boolean }) {
  const stamp = useStamp();
  const url = useStampAssetPreviewUrl(asset.id);

  return (
    <button
      type="button"
      className="button"
      title={`Place “${asset.label}”`}
      aria-pressed={armed}
      onClick={() => (armed ? stamp.disarm() : void stamp.armAsset(asset.id))}
    >
      {url ? <img src={url} alt={asset.label} className="preview" /> : asset.label}
    </button>
  );
}

function Marks() {
  const stamp = useStamp();
  const [library] = useStampLibraries();
  const marks = useStampAssets({ libraryId: library?.id });
  const { armedAsset } = useStampState();
  const [text, setText] = useState('Ada L.');
  const made = useRef(false);

  // On load: a drawn signature and typed initials, in a library of their own.
  useEffect(() => {
    if (made.current) return;
    made.current = true;
    void stamp.createLibrary('Ada Lovelace').then(async ({ library }) => {
      await stamp.createAsset({
        libraryId: library.id,
        label: 'Signature',
        mark: { kind: 'ink', strokes: [loop, underline], strokeWidth: 2.5, color: '#1d2b53' },
      });
      await stamp.createAsset({
        libraryId: library.id,
        label: 'Initials',
        mark: { kind: 'text', text: 'AL', fontFamily: 'times-italic', color: '#1d2b53' },
      });
    });
  }, [stamp]);

  const typeMark = async () => {
    if (!library || !text.trim()) return;
    const { asset } = await stamp.createAsset({
      libraryId: library.id,
      label: text.trim(),
      mark: { kind: 'text', text: text.trim(), fontFamily: 'times-italic', color: '#1d2b53' },
    });
    await stamp.armAsset(asset.id);
  };

  return (
    <div className="toolbar">
      {marks.map((asset) => (
        <MarkButton key={asset.id} asset={asset} armed={armedAsset?.id === asset.id} />
      ))}
      <span className="spacer" />
      <form
        className="type"
        onSubmit={(event) => {
          event.preventDefault();
          void typeMark();
        }}
      >
        <input
          className="field"
          aria-label="Text of a typed stamp"
          value={text}
          onChange={(event) => setText(event.target.value)}
        />
        <button type="submit" className="button" disabled={!library || !text.trim()}>
          Type a stamp
        </button>
      </form>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <Marks />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}

Place a stamp with code#

A click after armAsset() and a call to placeAsset() make the same annotation from the same inputs:

Loading live preview…

import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage, useStageState } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import {
  AnnotationLayer,
  annotationPlugin,
  useAnnotationList,
  useAnnotationState,
} 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 './programmatic.css';

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

// The middle of a Letter page, and the cover's empty corner, in page coordinates.
const MIDDLE = { x: 306, y: 396 };
const CORNER = { x: 60, y: 590, width: 220, height: 180 };

function PlaceStamps() {
  const stamp = useStamp();
  const stage = useStage();
  const assets = useStampAssets();
  const ready = useAnnotationState((state) => state.status === 'ready');
  const currentPage = useStageState((state) => state.currentPageIndex);
  const stamps = useAnnotationList({ subtype: 'stamp' });
  const placed = useRef(false);

  const approved = assets.find((asset) => asset.name === 'Approved');
  const draft = assets.find((asset) => asset.name === 'Draft');

  // On load: the standard stamps, and "Approved" in the cover's empty corner, scrolled into view.
  useEffect(() => {
    if (!ready || placed.current) return;
    placed.current = true;
    void loadDefaultLibrary('en')
      .then((bytes) => stamp.importLibrary(bytes))
      .then(({ library }) => {
        const asset = stamp
          .listAssets({ libraryId: library.id })
          .find((candidate) => candidate.name === 'Approved');
        if (!asset) return;
        return stamp.placeAsset(asset.id, {
          page: 0,
          center: { x: 170, y: 680 },
          targetWidth: 180,
          rotation: -8,
        });
      })
      .then(() => stage.reveal(0, { rect: CORNER }));
  }, [stamp, stage, ready]);

  return (
    <div className="toolbar">
      <button
        type="button"
        className="button"
        disabled={!approved}
        onClick={() =>
          approved &&
          void stamp.placeAsset(approved.id, { page: currentPage, center: MIDDLE, select: true })
        }
      >
        Approve this page
      </button>
      <button
        type="button"
        className="button"
        disabled={!draft}
        onClick={() =>
          draft &&
          void stamp.placeAssetOnPages(draft.id, 'all', {
            center: MIDDLE,
            targetWidth: 320,
            rotation: -30,
          })
        }
      >
        “Draft” on every page
      </button>
      <span className="spacer" />
      <output className="readout">
        {stamps.length} {stamps.length === 1 ? 'stamp' : 'stamps'} in the document
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <PlaceStamps />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
const { annotation } = await stamp.placeAsset(assetId, {
  page: page.ref,
  center: { x: 120, y: 90 }, // where the stamp's middle goes, in page coordinates
  targetWidth: 160, // optional: the stamp's own size by default
  rotation: 0, // optional: degrees clockwise
});
 
// The same stamp on several pages, such as "DRAFT" on every page
await stamp.placeAssetOnPages(assetId, 'all', { center: { x: 300, y: 400 } });

The box keeps the stamp’s proportions and stays on the page. The placed annotation carries the stamp’s identifier as its name. A placement from code is selected only when you ask, with select: true, like any annotation you create.

Make a stamp from annotations#

Select annotations on a page and turn them into a stamp. They become one stamp exactly as they look on the page, vector and all:

Loading live preview…

import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import {
  AnnotationLayer,
  annotationPlugin,
  useAnnotation,
  useAnnotationState,
} from '@embedpdf/react/annotation';
import {
  stampPlugin,
  useStamp,
  useStampAssetPreviewUrl,
  useStampAssets,
  useStampState,
} from '@embedpdf/react/stamp';
import type { StampAsset } from '@embedpdf/react/stamp';
import { localEngine } from '@embedpdf/engine';

import './from-selection.css';

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';
// An empty corner of the cover, in page coordinates.
const CORNER = { x: 70, y: 600, width: 190, height: 64 };

// On load: a framed text stamp in the cover's empty corner, both parts selected, scrolled into view.
function DrawSeal() {
  const annotation = useAnnotation();
  const stage = useStage();
  const ready = useAnnotationState((state) => state.status === 'ready');
  const drawn = useRef(false);

  useEffect(() => {
    if (!ready || drawn.current) return;
    drawn.current = true;
    void Promise.all([
      annotation.create(0, { subtype: 'square', box: CORNER, color: '#c4262e', strokeWidth: 4 }),
      annotation.create(0, {
        subtype: 'free-text',
        intent: 'free-text',
        box: CORNER,
        contents: 'CHECKED',
        fontFamily: 'helvetica-bold',
        fontSize: 30,
        textAlign: 'center',
        verticalAlign: 'middle',
        fontColor: '#c4262e',
        strokeWidth: 0,
      }),
    ]).then((created) => {
      annotation.selection.set(created.map((made) => made.annotation.ref));
      stage.reveal(0, { rect: CORNER });
    });
  }, [annotation, stage, ready]);

  return null;
}

function MyStamp({ asset, armed }: { asset: StampAsset; armed: boolean }) {
  const stamp = useStamp();
  const url = useStampAssetPreviewUrl(asset.id);

  return (
    <button
      type="button"
      className="button"
      title={`Place “${asset.label}”`}
      aria-pressed={armed}
      onClick={() => (armed ? stamp.disarm() : void stamp.armAsset(asset.id))}
    >
      {url ? <img src={url} alt={asset.label} className="preview" /> : asset.label}
    </button>
  );
}

function MakeStamp() {
  const stamp = useStamp();
  const selected = useAnnotationState((state) => state.selected);
  const mine = useStampAssets({ libraryId: MY_STAMPS });
  const { armedAsset } = useStampState();

  // A stamp is one page of artwork: the selection must be on one page.
  const page = selected[0]?.page;
  const onePage = selected.every(
    (annotation) => annotation.page.objectNumber === page?.objectNumber,
  );
  const canMake = !!page && onePage && stamp.canCreateFromAnnotations();

  const make = async () => {
    if (!page) return;
    // The library is made on first use.
    if (!stamp.getLibrary(MY_STAMPS)) await stamp.createLibrary('My stamps', { id: MY_STAMPS });
    const { asset } = await stamp.createAssetFromAnnotations(
      page,
      selected.map((annotation) => annotation.ref),
      { libraryId: MY_STAMPS, label: `My stamp ${mine.length + 1}` },
    );
    await stamp.armAsset(asset.id);
  };

  return (
    <div className="toolbar">
      <button type="button" className="button" disabled={!canMake} onClick={() => void make()}>
        Make a stamp of the selection
      </button>
      {mine.map((asset) => (
        <MyStamp key={asset.id} asset={asset} armed={armedAsset?.id === asset.id} />
      ))}
      <span className="spacer" />
      <output className="readout">
        {armedAsset ? 'Click a page to place it' : `${selected.length} selected`}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <DrawSeal />
        <MakeStamp />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
const { asset } = await stamp.createAssetFromAnnotations(page.ref, refs, {
  libraryId, // omit to create a library named after the label
  label: 'Custom stamp 1',
});

It’s all or nothing: a hidden annotation, one without an appearance, or one on another page fails the whole call.

Keep libraries between sessions#

The plugin tells you when a library changes and gives you its bytes; where you store them is up to you. persistStampLibraries and restoreStampLibraries connect a library to a store:

Loading live preview…

import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, saveFile } 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,
  useStamp,
  useStampAssetPreviewUrl,
  useStampAssets,
  useStampLibraries,
  useStampState,
} from '@embedpdf/react/stamp';
import type { StampAsset } from '@embedpdf/react/stamp';
import { localEngine } from '@embedpdf/engine';

import './persist.css';

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 libraries live between visits: this browser's IndexedDB.
const store = indexedDbByteStore('embedpdf-stamp-example');

function StampButton({ asset, armed }: { asset: StampAsset; armed: boolean }) {
  const stamp = useStamp();
  const url = useStampAssetPreviewUrl(asset.id);

  return (
    <button
      type="button"
      className="button"
      title={`Place “${asset.label}”`}
      aria-pressed={armed}
      onClick={() => (armed ? stamp.disarm() : void stamp.armAsset(asset.id))}
    >
      {url ? <img src={url} alt={asset.label} className="preview" /> : asset.label}
    </button>
  );
}

function Libraries() {
  const stamp = useStamp();
  const libraries = useStampLibraries();
  const assets = useStampAssets();
  const { armedAsset } = useStampState();
  const [restored, setRestored] = useState<number | null>(null);
  const started = useRef(false);

  // Every change is written to the store from now on.
  useEffect(() => persistStampLibraries(stamp, store), [stamp]);

  // On load: what the store kept. The first visit starts a library of its own.
  useEffect(() => {
    if (started.current) return;
    started.current = true;
    void restoreStampLibraries(stamp, store).then(async (ids) => {
      setRestored(ids.length);
      if (ids.length > 0) return;
      const { library } = await stamp.createLibrary('My stamps');
      await stamp.createAsset({
        libraryId: library.id,
        label: 'Checked',
        mark: { kind: 'text', text: 'Checked', fontFamily: 'times-bold-italic', color: '#1f7a3f' },
      });
    });
  }, [stamp]);

  const addStamp = async (libraryId: string) => {
    const label = `Stamp ${assets.length + 1}`;
    await stamp.createAsset({
      libraryId,
      label,
      mark: { kind: 'text', text: label, fontFamily: 'helvetica-bold', color: '#054fb3' },
    });
  };

  return (
    <div className="toolbar">
      {assets.map((asset) => (
        <StampButton key={asset.id} asset={asset} armed={armedAsset?.id === asset.id} />
      ))}
      {libraries.map((library) => (
        <span className="group" key={library.id}>
          <button type="button" className="button" onClick={() => void addStamp(library.id)}>
            Add a stamp
          </button>
          <button
            type="button"
            className="button"
            title="The library as the PDF it is: open it in Acrobat, or import it again"
            onClick={() =>
              void stamp
                .exportLibrary(library.id)
                .then((bytes) => saveFile(bytes, `${library.name}.pdf`, 'application/pdf'))
            }
          >
            Download “{library.name}”
          </button>
        </span>
      ))}
      <span className="spacer" />
      <output className="readout">
        {restored === null
          ? 'Restoring…'
          : `${restored} restored: add a stamp, then reload the page`}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <Libraries />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
persist.tsx
import { useEffect } from 'react';
import {
  indexedDbByteStore,
  persistStampLibraries,
  restoreStampLibraries,
  useStamp,
  useStampLibraries,
} from '@embedpdf/react/stamp';

const store = indexedDbByteStore('stamps');

export function StampLibraries() {
  const stamp = useStamp();
  const libraries = useStampLibraries();

  useEffect(() => {
    void restoreStampLibraries(stamp, store); // import every stored library
    return persistStampLibraries(stamp, store, { except: ['embedpdf-standard'] });
  }, [stamp]);

  return (
    <ul>
      {libraries.map((library) => (
        <li key={library.id}>{library.name}</li>
      ))}
    </ul>
  );
}

A store has three calls, list, put and delete, over bytes by library id. indexedDbByteStore is the browser default, and memoryStampStore is for tests. Writes are batched per library, so a burst of edits saves once.

Dynamic stamps#

A stamp can carry form fields, such as the user’s name and today’s date. When the actions plugin has JavaScript on, the stamp fills them in when it’s placed, with the user’s identity and the current time, and the result is flattened into the placed artwork. The library itself never changes. stampPlugin({ dynamic: false }) keeps stamps as they are.

Standard stamps in eight languages#

@embedpdf/default-stamps has the standard set as one library per language (en, de, nl, fr, es, zh-CN, sv, ja), each with the same identifiers and translated labels. It loads from your own build, so nothing is fetched from anywhere else. negotiateLocale, from @embedpdf/react/i18n, picks the one closest to the reader’s language:

import { LOCALES, loadDefaultLibrary } from '@embedpdf/default-stamps/library';
 
const locale = negotiateLocale(LOCALES, navigator.languages) ?? 'en';
await stamp.importLibrary(await loadDefaultLibrary(locale));

Pick a language: the same stamps come back with their labels translated.

Loading live preview…

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 } from '@embedpdf/react/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/react/annotation';
import {
  stampPlugin,
  useStamp,
  useStampAssetPreviewUrl,
  useStampAssets,
  useStampLibraries,
  useStampState,
} from '@embedpdf/react/stamp';
import type { StampAsset } from '@embedpdf/react/stamp';
import { LOCALES, loadDefaultLibrary } from '@embedpdf/default-stamps/library';
import { localEngine } from '@embedpdf/engine';

import './languages.css';

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 StampButton({ asset, armed }: { asset: StampAsset; armed: boolean }) {
  const stamp = useStamp();
  const url = useStampAssetPreviewUrl(asset.id);

  return (
    <button
      type="button"
      className="button"
      title={asset.label}
      aria-pressed={armed}
      onClick={() => (armed ? stamp.disarm() : void stamp.armAsset(asset.id))}
    >
      {url ? <img src={url} alt={asset.label} className="preview" /> : asset.label}
    </button>
  );
}

function LanguagePicker() {
  const stamp = useStamp();
  const [library] = useStampLibraries();
  const assets = useStampAssets({ libraryId: library?.id });
  const { armedAsset } = useStampState();
  const [locale, setLocale] = useState('nl');

  // The library of the chosen language replaces the one before: the same
  // identifiers, translated labels. Dutch on load.
  useEffect(() => {
    let current = true;
    void loadDefaultLibrary(locale).then(async (bytes) => {
      if (!current) return;
      for (const old of stamp.listLibraries()) await stamp.deleteLibrary(old.id);
      if (current) await stamp.importLibrary(bytes);
    });
    return () => {
      current = false;
    };
  }, [stamp, locale]);

  return (
    <div className="toolbar">
      <select
        className="field"
        aria-label="Language"
        value={locale}
        onChange={(event) => setLocale(event.target.value)}
      >
        {LOCALES.map((code) => (
          <option key={code} value={code}>
            {code}
          </option>
        ))}
      </select>
      {assets.slice(0, 4).map((asset) => (
        <StampButton key={asset.id} asset={asset} armed={armedAsset?.id === asset.id} />
      ))}
      <span className="spacer" />
      <output className="readout">{library?.name ?? 'Loading…'}</output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <LanguagePicker />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <AnnotationLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}

Methods#

Everything useStamp() gives you.

MethodWhat it does
listLibraries({ kind? })Every library, or the ones of the kinds you ask for. In a component, use useStampLibraries({ kind? }). Library kinds
getLibrary(libraryId)One library, or null
listAssets({ libraryId? })Every stamp, or the ones in one library, in the order a picker shows them. In a component, use useStampAssets({ libraryId }). Show a stamp picker
getAsset(assetId)One stamp, or null
getAssetPreview(assetId), renderAssetPreview(assetId, { width })A stamp’s picture as { bytes, mimeType }, or null: the thumbnail at the previewWidth setting, or one drawn at any width. In a component, useStampAssetPreviewUrl(assetId) gives an image URL.
readAssetBytes(assetId)A copy of the stamp’s artwork as a one-page PDF, or null
createLibrary(name, { kind? })Creates an empty library. Resolves { library }. Library kinds
updateLibrary(libraryId, changes)Renames a library, or changes its categories. Resolves { library }.
deleteLibrary(libraryId)Removes a library and every stamp in it
importLibrary(bytes, { libraryKind? })Makes a library from a PDF: an Acrobat stamp file as it is, a plain PDF with one stamp per page. Resolves { library }. A library is a PDF
exportLibrary(libraryId)The library as the PDF it is, to store or to open in Acrobat
createAsset({ libraryId?, label, source })Adds a stamp from a one-page PDF, a PNG or a JPEG, or from a mark instead of source. Without libraryId, it gets a library of its own. Resolves { asset }. Draw or type a stamp
createAssetFromAnnotations(page, refs, { libraryId?, label })Turns annotations on a page into one stamp, exactly as they look. Resolves { asset }. Make a stamp from annotations
updateAsset(assetId, changes)Changes a stamp’s label, subject or categories; its identifier never changes. Resolves { asset }.
deleteAsset(assetId)Removes a stamp from its library
moveAsset(assetId, { libraryId })Moves a stamp to another library. Resolves { asset }, which has a new id there.
duplicateAsset(assetId, { label? })Copies a stamp within its library. Resolves { asset }.
armAsset(assetId, { targetWidth? })Arms a stamp: the next click on a page places it, with a preview under the pointer. Show a stamp picker
disarm()Disarms the stamp without placing it
placeAsset(assetId, { page, center, targetWidth?, rotation?, select? })Places a stamp without the pointer, its middle on center. Resolves { annotation }. Place a stamp with code
placeAssetOnPages(assetId, pages, { center, … })Places the same stamp at the same spot on several pages, or on 'all'. Resolves { applied, skipped, failed }: the placed stamps, and the pages that were skipped or failed.
canPlace(), canCreateFromAnnotations()Whether placing a stamp, and making one from annotations, are allowed. Permissions
canImport()Whether libraries can be imported here. It turns false once an import finds no local engine to open them in; see Settings.
getSettings(), updateSettings(changes), resetSettings()Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use useStampSettings(). Settings

Every method that returns a promise also takes a signal in its options, to cancel it.

State#

Everything useStampState() gives you. Your component re-renders when a value changes. The method in the last column reads the same value once.

FieldWhat it isRead once
armedAssetThe stamp the next click on this document places, or nullgetArmedAsset()

The libraries and their stamps come from useStampLibraries() and useStampAssets({ libraryId }). Keeping them apart means a component that only shows the armed stamp doesn’t re-render when a library changes.

Settings#

Set them when you register the plugin, and change them while the app runs with stamp.updateSettings({ … }); stamp.resetSettings() goes back to what you registered, and useStampSettings() gives you the current settings.

SettingWhat it doesDefault
assetEngineThe engine stamp libraries open inthe viewer’s engine (local)
previewWidthThe width of stamp previews, in pixels256
dynamicFill in dynamic stamps when they’re placedtrue

Events#

events.tsx
import { useState } from 'react';
import { useStampEvent } from '@embedpdf/react/stamp';

export function LibraryStatus() {
  const [message, setMessage] = useState('');

  useStampEvent(
    (stamp) => stamp.onLibraryChanged,
    ({ libraryId, reason }) => setMessage(`${libraryId}: ${reason}`),
  );

  return <p aria-live="polite">{message}</p>;
}
EventFires whenCarries
onLibraryChangedAnything in a library changed: the one to save it onlibraryId, reason
onLibraryCreated, onLibraryUpdated, onLibraryDeletedA library was added, changed or removedlibraryId, library
onAssetCreated, onAssetUpdated, onAssetDeletedA stamp was added, changed or removedassetId, libraryId, asset
onArmChangedA stamp was armed or disarmeddocumentId, asset
onSettingsChangedThe settings changedsettings, changed

reason is 'created', 'imported', 'updated', 'asset-added', 'asset-updated', 'asset-removed' or 'removed'. library and asset are the new values, and null in the deleted events; in the arm event, asset is null when the stamp was disarmed.

Permissions#

PermissionWhat it allowsCheck
annotations:createPlacing a stampstamp.canPlace()
doc.downloadMaking a stamp from annotations, which copies them outstamp.canCreateFromAnnotations()

Libraries live in the browser and touch no document, so creating and importing them needs no document permission.

Good to know#

The identifier#

A stamp made here gets an identifier in Acrobat’s own form, so it keeps its identity when its library is opened in Acrobat.

Stamps in the engine#

A placed stamp is a normal stamp annotation. To place one without the plugin, or copy one, see Stamps.

Was this page helpful?

Your feedback goes directly to the documentation team.