# What you can replace

Everything the SDK draws can be switched off, renamed, re-iconed, restyled or refilled, and the dialogs in the table below can be replaced by your own. The "License required" popup and the watermark are the exceptions: they enforce the license — the popup says it in your words ([`Kernstra.init({ licensePopup })`](https://kernstra.com/docs/web-sdk/config/init/licensepopup)), keeps its link to kernstra.com, and is not replaced. The stamp picker and "New stamp", the link editor, the note composer and the selection bar cannot be replaced — turn their tool off (`tools.stamp`, `tools.link`, `tools.note`, `tools.selectText`) and build your own over the programmatic API ([`PDFDocument`](https://kernstra.com/docs/web-sdk/api/pdfdocument), [`PDFViewer`](https://kernstra.com/docs/web-sdk/api/pdfviewer)) if you need a different one.

## Example

```ts
import type { SavedSignature, SavedStamp } from '@kernstra/pdf-web-sdk';

new PDFViewer(container, doc, {
  callbacks: {
    // Your signing provider instead of the draw / type / upload dialog.
    onSignatureRequest: async (field) => {
      const png = await mySigningProvider.capture({ field: field?.name });
      return png === undefined ? null : { dataUrl: png };
    },
    // Your asset library instead of the file picker.
    onImageRequest: async () => (await myAssetLibrary.pick({ accept: 'image/*' })) ?? null,
    // Links go through your own router and warning.
    onExternalLink: (url) => {
      void myRouter.confirmAndOpen(url);
    },
  },
});

// Keeping saved signatures and stamps: store what the events give you, pass it back next time.

const load = <T>(key: string): T[] => {
  try {
    const value: unknown = JSON.parse(localStorage.getItem(key) ?? '[]');
    return Array.isArray(value) ? (value as T[]) : [];
  } catch {
    return [];
  }
};
const save = (key: string, list: unknown): void => {
  try {
    localStorage.setItem(key, JSON.stringify(list));
  } catch {
    // Storage full or blocked: the list still lasts for this session.
  }
};

const viewer = new PDFViewer(container, doc, {
  tools: {
    sign: { savedSignatures: load<SavedSignature>('signatures') },
    stamp: { userStamps: load<SavedStamp>('stamps') },
  },
});
viewer.on('savedSignaturesChanged', ({ signatures }) => save('signatures', signatures));
viewer.on('userStampsChanged', ({ stamps }) => save('stamps', stamps));
```

## You want to…

| You want to… | Use |
| --- | --- |
| Hide a tool, everywhere | `tools.<name>: false` — the toolbar, the selection bar and `viewer.activateTool()` all follow it |
| Hide a group of tools | `toolbar.groups` (ribbon tabs, and the bar's tools) |
| Hide a piece of chrome | `toolbar.print` / `download` / `history` / `offlineNotice`, `navigation.*` switches, `sidebar.<panel>: false`, `sidebar.rail: false`, `export.successToast`, `forms.stepper`, `links.pill`, `tools.stamp.builder` |
| Ask, or not, before a deletion | `confirmations` — all of them, or one question at a time; `callbacks.onConfirm` for your own dialog — see [Confirmations](https://kernstra.com/docs/web-sdk/config/confirmations) |
| Draw your own toolbar | `toolbar.layout: 'none'` and the programmatic API; `sidebar.rail: false` for your own panel buttons — see [Bring your own chrome](https://kernstra.com/docs/web-sdk/recipes/own-chrome) |
| Word the "License required" popup | [`Kernstra.init({ licensePopup })`](https://kernstra.com/docs/web-sdk/config/init/licensepopup) — its title, text and Close label; the link stays kernstra.com. See [Licensing](https://kernstra.com/docs/web-sdk/guides/licensing) |
| Change any word | `locale.strings` / `locale.translations` — see [Locale](https://kernstra.com/docs/web-sdk/config/locale) |
| Change any icon | `toolbar.icons` — see [Toolbar](https://kernstra.com/docs/web-sdk/config/toolbar/icons) |
| Change colors, type, spacing | CSS tokens (`--ks-*`) and `theme` — see [Theme tokens](https://kernstra.com/docs/web-sdk/guides/theme-tokens) |
| Change what the pickers offer | `annotations.palette` / `highlightColors` / `noteColors` / `strokeWidths` / `markSizes`, `fonts.sizes`, `navigation.zoomPresets`, `tools.stamp.presets` / `inks`, `tools.sign.inks` / `fonts`, `tools.image`, `tools.erase` |
| Know what the user did | `viewer.on('contentPlaced')`, `annotationsChanged`, `commentsChanged`, `textChanged`, `formFieldChanged`, `signatureCaptured`, `documentChanged`, `pagesChanged`, `toolChanged` — see [Events](https://kernstra.com/docs/web-sdk/api/events) |

## Dialogs you can replace

| Dialog | Replace it with |
| --- | --- |
| Signature (tool and fields) | `callbacks.onSignatureRequest(field \| null)` → `{ dataUrl }` or `null`. The SDK's dialog never opens |
| Image picker | `callbacks.onImageRequest()` → a `Blob`/`File` or `null`, checked against `tools.image` |
| "Leave the document?" | `callbacks.onExternalLink(url)` — you get the checked address; the SDK neither asks nor opens |
| "Delete this page?" and the other confirmations | `callbacks.onConfirm(request)` → `true` to go ahead; or switch questions off in `confirmations` |
| Download | `callbacks.onDownload()`, with `viewer.createExporter()` for the bytes; or `export.askOnDownload: false` |
| Print | `callbacks.onPrint()`, with `viewer.print({ pages })`; or `print.ask: false` |
| Security | `tools.security: false` and your own form over [`doc.security`](https://kernstra.com/docs/web-sdk/api/doc-security) |
| Manage Pages | `tools.managePages: false` and your own grid over [`doc.pages`](https://kernstra.com/docs/web-sdk/api/doc-pages) |
| Password, load error, drop zone | They are yours to call — `createPasswordPrompt`, `createLoadError`, `createDropZone` — or not; see [Document states](https://kernstra.com/docs/web-sdk/api/helpers) |
| Loading indicator | `loader.html` — see [Loader](https://kernstra.com/docs/web-sdk/config/loader) |

## Notes

**Dialogs you can replace.** A replaced dialog is replaced wherever the SDK would open it: the signature hook answers both the Sign tool (`field` is `null`) and a form's signature fields (`field` is that field, and `signatureCaptured` reports `method: 'host'`). See [Callbacks](https://kernstra.com/docs/web-sdk/config/callbacks). Your own security panel, with the Security tool off: `new PDFViewer(container, doc, { tools: { security: false } })`, then on your lock button `await doc.security.protect({ userPassword: form.openPassword, ownerPassword: form.ownerPassword, permissions: { print: true, copy: false, modify: false } })`.

**Keeping saved signatures and stamps.** The signatures a user saves for reuse and the stamps they make with "Create a stamp" last for the page's session: the SDK stores nothing on disk, because a likeness of someone's signature is yours to decide about. To keep them, store what the [`savedSignaturesChanged`](https://kernstra.com/docs/web-sdk/api/events#ev-savedsignatureschanged) and [`userStampsChanged`](https://kernstra.com/docs/web-sdk/api/events#ev-userstampschanged) events give you and pass it back next time, as the example above does. Both lists are plain JSON: a `SavedSignature` is `{ dataUrl: string; initialsDataUrl?: string | null; name?: string; method: 'draw' | 'type' | 'upload' }` (the signature as an image data URL; the initials, when the user made them; the typed name, for a typed signature); a `SavedStamp` is either `{ kind: 'text'; label: string; color: string; dated: boolean; border: 'boxed' | 'ribbon' | 'plain' }` (the wording, `'#RRGGBB'`, the placing date under the wording) or `{ kind: 'image'; label: string; dataUrl: string; widthPt: number; heightPt: number }` (the picture as an image data URL and its size on the page, in points). Both types are exported as `SavedSignature` and `SavedStamp`.

Both events carry the whole list after every change. The lists are the page's, not the viewer's: every viewer on the page reports the same change. An exception thrown by a handler (a full `localStorage`, say) is logged with `console.error`; the user's signature or stamp is kept all the same.

At most three signatures and eight stamps are kept. A restored list is not trusted — it comes from storage the page does not control: an entry that is not an object, a signature whose `dataUrl` is not an image data URL (PNG, JPEG, WebP, GIF or BMP, at most 10 MB), a text stamp without a label or a hex color, or a picture stamp without an image data URL and positive sizes, is skipped (a picture stamp whose image passes the check but does not decode leaves the list once it fails, and `userStampsChanged` reports the shorter list); skipped entries do not count toward the limit. Initials that are not an image data URL are dropped, and a missing `name` or unknown `method` takes a neutral value. A value that is not a list is ignored with a console warning.

**Several viewers on one page** share these two lists: a signature saved in one is offered in all. The list a viewer is constructed with replaces the page's, including anything saved in viewers already open, so pass the same, current list to each. Each viewer that has a change callback calls it, so a page with two viewers hears each change twice — harmless when the callback only stores the list.

**Watching what the user adds.** [`contentPlaced`](https://kernstra.com/docs/web-sdk/api/events#ev-contentplaced) fires once for each new thing the user adds with a tool, added text included — when it is committed to the document, not while it is being drawn, and not again when it is moved, resized or edited. A duplicate is a new thing and fires. A highlight made from the selection bar reports `'textHighlight'`, the Mark text tool's name; an underline, squiggly underline, strikeout or black-out made there reports `'selectText'`. Signing a form field is not a tool and does not fire it — that is `signatureCaptured`. Undo does not retract it (it fires `documentChanged` with `source: 'undo'`, and `commentsChanged` where comments differ): a host that mirrors what was added should re-read the page's annotations (`doc.annotations.list(pageIndex)`) rather than count events. The event names the tool and the page, not the annotation itself: `viewer.on('contentPlaced', ({ tool, pageIndex }) => audit.log(tool, pageIndex + 1))`.

Editing existing text reports through [`textChanged`](https://kernstra.com/docs/web-sdk/api/events#ev-textchanged) instead: one event when Edit Text writes a session's edits into the document — on Done, on leaving the tool any other way, before an undo, or from `viewer.tools.editText.commit()` — with every block it wrote, however many. Each change names the page, the block (the id `listBlocks()` gave it in the session that ended; the blocks are detected afresh after a commit, so it names the block for a log, not for a later call), what changed — `'text'` for the words, whatever changed with them; `'style'` for the font, size, weight, slant or color alone; `'box'` for a resize or a move alone; `'deleted'` — and the block's text before and after, each with its lines joined by one space: the same text for a `'style'` or `'box'` change, an empty `newText` for a deleted block. A commit that wrote nothing reports nothing; one whose save failed reports its blocks with the commit that saves them. Under a license without `extract` the texts are empty, as form values are. It is the audit event for text: a host that saves as the user works listens to `documentChanged` instead, which carries no text. `viewer.on('textChanged', ({ changes }) => { for (const { pageIndex, kind, oldText, newText } of changes) audit.log(pageIndex, kind, oldText, newText); })`.

**Saving as the user works.** [`documentChanged`](https://kernstra.com/docs/web-sdk/api/events#ev-documentchanged) fires once for every change to the document, after it is in the document, whatever made it — a tool, Manage Pages or a thumbnail's menu, the outline, attachments or comments panel, a form field filled in or signed, the document API, undo or redo — and carries only what made it and the viewer's state after it: `source` (and `tool` for a tool's change, where one is active), `hasUnsavedChanges`, `canUndo`, `canRedo`. It is the hook for autosave, for a dirty flag and for your own undo and redo buttons; nothing in it is the document's content, so there is nothing to blank under a license without `extract` and nothing large to log. A history step that left the document as it was — a snapshot of nothing, a move back to where it started — reports nothing. A form field is outside the history and reports every commit of its value — each keystroke, as `formFieldChanged` does, and a value set again: debounce the save. `'api'` is the document API — the host's call, and the Security tool's, which writes through `doc.security`. A bookmark or attachment the host writes through `doc.outline` or `doc.attachments` reaches a viewer on the document like any other API change: its panel refreshes, it takes an undo step and `documentChanged` fires; the panels' own edits do the same.

[`viewer.hasUnsavedChanges`](https://kernstra.com/docs/web-sdk/api/pdfviewer/hasunsavedchanges) is the same flag read directly: `true` from the first change until the document is exported whole by an exporter from `viewer.createExporter()` — the toolbar's Download, or `download()`, `getBlob()`, `getArrayBuffer()`, `getBase64()` or `uploadTo()` without `pages` — or until you call [`viewer.markSaved()`](https://kernstra.com/docs/web-sdk/api/pdfviewer/marksaved) after saving your own way (`doc.save()`, an upload of bytes you already had). "Saved" means the bytes left the viewer: a change made while an export ran is not in them and stays unsaved, and an undo back to the saved state still counts, since what you saved is not what is shown until it is saved again. Whether your upload of those bytes succeeded is yours to track — the flag does not go back to `true` on a failed upload. Nothing fires when it goes back to `false`: the export's `exportCompleted`, or your own `markSaved()`, is that moment. The page itself is yours to guard, with a `beforeunload` handler that checks `viewer.hasUnsavedChanges`, your own upload-failed flag and `viewer.tools.editText.hasPendingEdits` — see [Unsaved changes](https://kernstra.com/docs/web-sdk/recipes/unsaved-changes) for the full autosave pattern.

`textChanged`, `contentPlaced`, `commentsChanged` and `formFieldChanged` still say **what** changed, for an audit trail or a backend that syncs piece by piece; `documentChanged` says **that** it changed.


Web page: https://kernstra.com/docs/web-sdk/guides/replace
