What you can replace
Kernstra.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, PDFViewer) if you need a different one.Example
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 |
| Draw your own toolbar | toolbar.layout: 'none' and the programmatic API; sidebar.rail: false for your own panel buttons — see Bring your own chrome |
| Word the "License required" popup | Kernstra.init({ licensePopup }) — its title, text and Close label; the link stays kernstra.com. See Licensing |
| Change any word | locale.strings / locale.translations — see Locale |
| Change any icon | toolbar.icons — see Toolbar |
| Change colors, type, spacing | CSS tokens (--ks-*) and theme — see 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 |
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 |
callbacks.onPrint(), with viewer.print({ pages }); or print.ask: false | |
| Security | tools.security: false and your own form over doc.security |
| Manage Pages | tools.managePages: false and your own grid over doc.pages |
| Password, load error, drop zone | They are yours to call — createPasswordPrompt, createLoadError, createDropZone — or not; see Document states |
| Loading indicator | loader.html — see 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. 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 and 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 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 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 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 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() 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 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.