The viewer hosts an E-Sign preparation layered on top of the open PDF. The preparation is a collection of index.ESignRoles, each owning placeholder index.ESignFields the signer will fill before the document is signed.
The preparation is not persisted into the PDF until the signer actually signs — it lives on the document view until then. When the sign action runs, the viewer bakes the touched fields into a copy of the open document and ships that file to the signing API; the original document the user is looking at is left untouched.
import type { ESignDocument, ESignRole, ESignField, FieldType, ESignMarkerRenderData } from '@avanquest/pdf-web-viewer';
| Type | Purpose |
|---|---|
ESignDocument |
Root preparation object: a list of roles (+ optional active-marker customization). |
ESignRole |
One signer / participant. Carries name, color, and per-variant artwork. |
ESignField |
A single placeholder on a page (rect + FieldType + value + touched). |
FieldType |
Enum discriminating signature / filler / metadata variants. |
ESignFieldOptions |
Per-variant options (e.g. dropdown items, radio group key, max length). |
ESignMarkerRenderData |
Appearance of the active "next field" marker (fill / stroke / content + paddings). |
ESignFieldRenderData |
Appearance of an on-page field placeholder (fill / stroke / content + borderRadius). |
ESignMarkerContent |
The shared content: text + typography. |
touched — the heart of the flowEvery index.ESignField carries a boolean touched flag. It distinguishes
an empty placeholder (drawn as a styled outline) from a field the signer
has interacted with (drawn with its filled value, baked into the signed PDF).
touched to pick the draw style.isRequired field is still untouched.There are three flow categories. The FieldType value determines which path
runs at sign time.
FieldType |
What it draws | How it bakes into the signed PDF |
|---|---|---|
Signature |
role.signature |
Image is inserted into the page's content stream (permanent pixels, no editable annotation). Aspect ratio preserved, centered in the rect. |
Initials |
role.initials |
Same as Signature, using the role's initials artwork. |
DigitalSignature |
role.digitalSignature |
Flows through the sign API as a signature placement and produces a real digital signature in the output PDF. |
Certify |
role.certify |
Flows through the sign API as the certifying signature (only one allowed per document). |
Visible vs. cryptographic.
Signature/Initialsare visual placements only — the appearance is baked into the page. ADigitalSignature(orCertify) is required for the output to be actually digitally signed. If the package only contains visibleSignature/Initialsfields, the viewer auto-injects a near-invisible 1pt placeholder digital signature so the sign API still produces a signed PDF.
Certify is a signature variant with two extra rules:
AllowFormFill) that constrains what downstream
signers / readers can change without invalidating the certification.FieldType |
UI | Baked as |
|---|---|---|
TextInput |
In-place text editor (optional multiline, max length) | Flattened Text widget. |
DropDown |
Single-select dropdown | Flattened Text widget with the chosen value. |
CheckBox |
Single checkbox | Flattened CheckBox widget (ZapfDingbats ✓ glyph). |
RadioButton |
Mutually-exclusive radio group (key from options.radioGroup) |
Flattened RadioButton widget (ZapfDingbats ● glyph). |
Date |
In-place text editor — on first click pre-filled with today's date formatted by role.dateFormat; signer can edit at the caret position. |
Flattened Text widget with the formatted date. |
Name / Title / Company / Email |
In-place text editor — on first click pre-filled from the role's profile (role.name / role.title / role.companyName / role.email); signer can immediately edit at the caret position. |
Flattened Text widget. |
Each touched filler is created as a form widget on a copy of the document, has its value set on the parent acroform, and is then flattened into page content. The signed PDF contains the rendered value as page content — there's no editable form field a downstream signer could change.
Untouched fields never appear in the signed PDF, regardless of FieldType.
They're placeholders only.
The index.IDocumentViewModel exposes three methods:
const view = editor.ui.pdfWebService.getActiveDocumentViewElement()?.documentView;
view.getESignDocument(): ESignDocument | null;
view.setESignDocument(doc: ESignDocument | null, validate?: { security: boolean; fileType: boolean; maxSize?: number }): boolean;
view.eSignDocumentChanged(): Observable<ESignDocument | null>;
import type { ESignDocument } from '@avanquest/pdf-web-viewer';
const view = editor.ui.pdfWebService.getActiveDocumentViewElement().documentView;
/* Activate a fresh, empty preparation on the active view. The `validate`
* gates run the host PDF's E-Sign eligibility checks BEFORE the preparation
* is attached. Returns false (and shows a warning dialog) when the PDF is
* encrypted / restricted, the file type is unsupported, or it exceeds
* `maxSize` bytes. */
const ok = view.setESignDocument({ roles: [] }, { security: true, fileType: true, maxSize: 50 * 1024 * 1024 });
if (!ok) {
/* validation rejected — preparation NOT started */
return;
}
/* Tear the preparation down (e.g. after a successful sign, or on cancel).
* Passing null skips validation. */
view.setESignDocument(null);
const sub = view.eSignDocumentChanged().subscribe((doc) => {
if (!doc) {
console.log('E-Sign preparation cleared');
return;
}
const total = doc.roles.reduce((n, r) => n + r.fields.length, 0);
const filled = doc.roles.reduce((n, r) => n + r.fields.filter((f) => f.touched).length, 0);
console.log(`E-Sign progress: ${filled}/${total} fields touched`);
});
The observable emits whenever the preparation is replaced or cleared, and whenever the view model invalidates after an in-place mutation (a field is added / removed, a field's role changes, the signer types into a field, etc.) — so subscribers can treat each emission as "redraw if needed."
Two optional hooks on the index.ESignDocument let you fully restyle what
the viewer paints — both build on the same shared visual fields
(fill? / stroke? / content?):
| Hook | Styles | Returns |
|---|---|---|
getMarkerRenderData |
The floating "next field" marker. | index.ESignMarkerRenderData |
getFieldRenderData |
An on-page field placeholder. | index.ESignFieldRenderData |
Both follow the same all-or-nothing contract: when set, the return value is
used verbatim — the renderer never mixes in its own defaults; when absent,
it draws its built-in default. There is no per-field fallback, so return every
field you care about. The shared content object (text + typography) is itself
all-or-nothing: include it in full, or omit it for no text.
getMarkerRenderDataWhile the signer is in view mode, the viewer draws a floating marker — a colored flag pointing at the next field that still needs filling (the next required-but-untouched field, or, once all required fields are done, the next optional one). By default the flag is filled with the role color and shows the field-type glyph.
import type { ESignMarkerRenderData } from '@avanquest/pdf-web-viewer';
import { EFontStyle } from '@avanquest/pdf-web-viewer';
const doc = view.getESignDocument();
if (doc) {
doc.getMarkerRenderData = (role, field): ESignMarkerRenderData => ({
fill: role.color, // body color; omit for a transparent body
stroke: '#ffffff', // optional outline; omit for none
paddingX: 9.2, // horizontal padding around the text (sets / grows the flag length)
paddingY: 7.2, // vertical padding above & below the text (sets the flag thickness)
content: {
// omit `content` entirely for a textless flag
text: 'Sign here', // a single glyph for an icon, or any string
color: '#ffffff',
fontFamily: 'Poppins, Roboto, sans-serif',
fontSize: 12, // view px
fontStyle: EFontStyle.BOLD,
},
});
view.notifyESignDocumentChanged(); // redraw with the new marker
}
The text drives the geometry, not the other way around:
content.fontSize + 2 * paddingY.content.fontSize + 2 * paddingX, and
grows to fit the measured text width plus paddingX on each end. A long
content.text stretches the flag; it is never clipped.content, the font term is 0: the flag collapses to just the
paddings and draws empty — no glyph, no placeholder box.| Field | Required | Meaning |
|---|---|---|
fill |
no | Body fill color. Omit for a transparent body. |
stroke |
no | Outline color. Omit for no stroke. |
paddingX |
yes | Horizontal padding around the text (sets / grows flag length). |
paddingY |
yes | Vertical padding above & below the text (sets flag thickness). |
content |
no | Text + typography ( index.ESignMarkerContent). Omit for a textless flag. |
index.ESignMarkerContent (when present, every field is required):
| Field | Meaning |
|---|---|
text |
Glyph or string drawn in the body. |
color |
Text color. |
fontFamily |
Font family for text. |
fontSize |
Font size in view px (also seeds the flag size). |
fontStyle |
EFontStyle (regular / bold / italic / bold-italic). |
getFieldRenderDataAn untouched field is drawn on the page as a placeholder (by default: a
low-alpha role-color wash, the field-type icon, and a label). Set
getFieldRenderData to replace that look per field. It only affects the
placeholder — once a field is touched, its real value (signature image, typed
text, checkbox state…) is drawn instead.
import type { ESignFieldRenderData } from '@avanquest/pdf-web-viewer';
import { EFontStyle } from '@avanquest/pdf-web-viewer';
const doc = view.getESignDocument();
if (doc) {
doc.getFieldRenderData = (role, field): ESignFieldRenderData => ({
fill: role.color, // box fill; omit for transparent
fillOpacity: 0.1, // applied to `fill` only; omit for opaque
stroke: role.color, // box outline; omit for none
borderRadius: 6, // view px; omit for square corners
showIcon: true, // draw the field-type glyph at the left edge
content: {
// omit `content` for an empty box
text: 'Click to sign',
color: role.color,
fontFamily: 'Poppins, Roboto, sans-serif',
fontSize: 11,
fontStyle: EFontStyle.REGULAR,
},
});
view.notifyESignDocumentChanged();
}
The default placeholder is itself an ESignFieldRenderData drawn through this
same path — a low-opacity role-color wash, showIcon: true, and the field-type
label as content.
The label is fit to the field: it auto-shrinks so it stays inside the box,
treating content.fontSize as the maximum size (it is never drawn larger,
but may render smaller in a narrow field).
| Field | Required | Meaning |
|---|---|---|
fill |
no | Box fill color. Omit for a transparent box. |
fillOpacity |
no | Opacity (0..1) applied to fill only. Omit (or 1) for opaque. |
stroke |
no | Outline color. Omit for no stroke. |
borderRadius |
no | Corner radius (view px) of the box. Omit for square corners. |
showIcon |
no | Draw the field-type glyph at the left edge; content then sits to its right. |
content |
no | Text + typography ( index.ESignMarkerContent). Omit for an empty box. |
An index.ESignField stores its position as left / top / width /
height in the E-Sign coordinate space: page-local, origin at the page's
top-left corner, Y growing down, at unit scale (independent of zoom / DPR). To
place a field where the user clicked, convert the pointer position (view /
device coordinates) into that space.
Three coordinate systems are involved:
| Space | Origin / axes | Produced by |
|---|---|---|
| View | viewport top-left, device pixels | the DOM event (pointer / click) |
| Page | PDF user space, bottom-left, Y up | index.IDocumentViewModel.mapPointToPage |
| E-Sign | page-local top-left, Y down, unit | index.IPageModel.getPageMatrix |
The conversion chain is view → page → E-Sign:
import type { IDocumentViewModel, PointModel } from '@avanquest/pdf-web-viewer';
/**
* Convert a point in view (device/canvas) coordinates — e.g. a click — into the
* coordinate space `ESignField.left` / `.top` are stored in.
* Returns `null` when the point is not over any page.
*/
function viewPointToESign(view: IDocumentViewModel, viewPoint: PointModel): { pageIndex: number; point: PointModel } | null {
const pageIndex = view.getPageAtPoint(viewPoint);
if (pageIndex < 0) return null; // click landed outside every page
// view → PDF page space (bottom-left origin, Y up)
const pagePoint = view.mapPointToPage(pageIndex, viewPoint);
// page → E-Sign space (top-left origin, Y down, unit scale)
const page = view.getDocument().getPage(pageIndex);
if (!page) return null;
const esignPoint = page.getPageMatrix().mapPoint(pagePoint);
return { pageIndex, point: esignPoint };
}
Using it to drop a field at the click position:
import { FieldType, type ESignField } from '@avanquest/pdf-web-viewer';
const view = editor.ui.pdfWebService.getActiveDocumentViewElement().documentView;
canvas.addEventListener('click', (e) => {
const viewPoint = { x: e.offsetX, y: e.offsetY };
const hit = viewPointToESign(view, viewPoint);
if (!hit) return;
const field: ESignField = {
id: crypto.randomUUID(),
parentId: null,
fieldType: FieldType.Signature,
value: null,
touched: false,
pageIndex: hit.pageIndex,
left: hit.point.x, // already in E-Sign space
top: hit.point.y,
width: 220,
height: 60,
isRequired: true,
};
const doc = view.getESignDocument();
doc?.roles[0]?.fields.push(field);
view.setESignDocument(doc);
});
Reverse direction (E-Sign → view). To position your own DOM overlay over a stored field, invert the chain:
page.getPageMatrix().invert()maps the field'sleft/topback to page space, then index.IDocumentViewModel.mapPointToDevice maps page space to view space. The matrix already accounts for the page's rotation.
import { PdfEditor, FieldType, type ESignDocument, type ESignField, type ESignRole } from '@avanquest/pdf-web-viewer';
const editor = await PdfEditor({
license: 'YOUR_LICENSE_KEY',
basePath: '/assets',
container: document.getElementById('viewer'),
});
await editor.ui.pdfWebService.openDocument(myPdfFile);
const view = editor.ui.pdfWebService.getActiveDocumentViewElement().documentView;
/* 1. Start an empty preparation, gated by eligibility checks. */
view.setESignDocument({ roles: [] }, { security: true, fileType: true, maxSize: 50 * 1024 * 1024 });
/* 2. Build a role and a few fields. (In the UI the toolbar drives this — this
* snippet shows the shape of the data model.) */
const role: ESignRole = {
id: 'signer-1',
color: '#1E88E5',
name: 'Jane Doe',
email: '[email protected]',
dateFormat: 'dd/MM/yyyy',
fields: [],
};
const signatureField: ESignField = {
id: 'f-sig-1',
parentId: null,
fieldType: FieldType.Signature,
value: null,
touched: false,
pageIndex: 0,
left: 72,
top: 600,
width: 220,
height: 60,
isRequired: true,
};
const nameField: ESignField = {
id: 'f-name-1',
parentId: null,
fieldType: FieldType.Name,
value: null,
touched: false,
pageIndex: 0,
left: 72,
top: 540,
width: 220,
height: 24,
isRequired: false,
};
role.fields.push(signatureField, nameField);
const doc: ESignDocument = { roles: [role] };
view.setESignDocument(doc);
/* 3. Signer interacts in the UI — the renderer flips `touched` on each
* interacted field and stores the entered value. You can also script it:
* importing setESignFieldValue() from the package is not part of the
* public API, so flip the flag yourself if you need to. */
nameField.value = 'Jane Doe';
nameField.touched = true;
/* 4. Reading the result before signing: who's done, who's not. */
const missingRequired = doc.roles.flatMap((r) => r.fields).filter((f) => f.isRequired && !f.touched);
if (missingRequired.length > 0) {
console.warn(`${missingRequired.length} required field(s) still untouched`);
}
/* 5. The Sign action is wired to the toolbar — it runs the sign API on a
* copy of the document, bakes touched widgets / signatures in, and then
* calls view.setESignDocument(null) on success. */
When the signer triggers the Sign action in the E-Sign toolbar:
isRequired fields are touched. If not, signing is
blocked with a warning.Signature, Initials) are inserted
into the signing copy's page content stream as images (aspect-preserved,
centered in the field rect).DigitalSignature and the optional Certify field are
collected into signing-API payloads.view.setESignDocument(null)) and the signed PDF is opened in the same
pane the user signed from.The output PDF therefore contains:
DigitalSignature / Certify field,Signature / Initials
field,Callbacks on ISetupOptions let you swap out the library's built-in E-Sign dialogs and signing pipeline without forking the toolbar:
| Callback | What it overrides | When it fires |
|---|---|---|
onESignFieldFillRequested |
The built-in signature-capture modal. | First click on an untouched Signature / Initials / Certify / DigitalSignature field. |
onESignFieldEditRequested |
(No default) lets you open a custom field-editing UI. | Double-click on an already-placed field while in edit mode. |
onESignSignRequested |
The built-in sign API pipeline behind the toolbar's Sign button. | After required-field validation passes, before the toolbar would normally call the bundled signing service. |
The fallback is always the library's default — if you don't wire a callback, nothing changes. If a callback throws or is misconfigured, the library logs and falls back so signing/filling never silently no-ops.
onESignFieldFillRequestedMirrors the same pattern as the Fill & Sign tab's
onAddSignature. The library
passes you the field the signer interacted with and the role that owns it.
You show your own UI, mutate the field's value / touched (and, for
signature variants, the matching role.signature / role.initials /
role.certify / role.digitalSignature slot), and resolve with true.
Resolving with false lets the library fall back to its default dialog.
import { PdfEditor, FieldType, type ESignField, type ESignRole } from '@avanquest/pdf-web-viewer';
await PdfEditor({
license: 'YOUR_LICENSE_KEY',
basePath: '/assets',
container: document.getElementById('viewer'),
onESignFieldFillRequested: async (field: ESignField, role: ESignRole) => {
/* Signature variants — capture or look up an HTMLImageElement and store
* it on the role; flip `touched` so the renderer paints the image. */
if (
field.fieldType === FieldType.Signature ||
field.fieldType === FieldType.Initials ||
field.fieldType === FieldType.Certify ||
field.fieldType === FieldType.DigitalSignature
) {
const image = await myCustomSignaturePicker.open({ role });
if (!image) return false; // user cancelled → use library default
if (field.fieldType === FieldType.Initials) role.initials = image;
else if (field.fieldType === FieldType.Certify) role.certify = image;
else if (field.fieldType === FieldType.DigitalSignature) role.digitalSignature = image;
else role.signature = image;
field.value = true;
field.touched = true;
return true; // library skips its default dialog
}
/* Anything else — let the library handle it (CheckBox / RadioButton
* toggles; TextInput / DropDown / Name / Company / Title / Email / Date
* in-place editors). */
return false;
},
});
Not invoked for:
CheckBox,RadioButton(click-toggles),TextInput,DropDown,Name,Company,Title,Date(in-place text editors — the last five pre-fill from the role profile or today's date and then behave like any other text field so the signer can immediately edit). Those never open a modal in the built-in flow, so there's no dialog to override.
onESignFieldEditRequestedFires when the author double-clicks an already-placed field while the
editor is in edit mode — the counterpart to onESignFieldFillRequested (the
signer's first-fill hook). Use it to open your own editing UI for the field:
change its value, options, or role. It fires for any field type and has no
library default, so a missing callback (or one that returns false / throws)
is simply a no-op. Mutate the field directly and resolve with true; the
library then redraws.
import { PdfEditor, type ESignField, type ESignRole } from '@avanquest/pdf-web-viewer';
await PdfEditor({
license: 'YOUR_LICENSE_KEY',
basePath: '/assets',
container: document.getElementById('viewer'),
onESignFieldEditRequested: async (field: ESignField, role: ESignRole) => {
const changes = await myFieldEditor.open({ field, role });
if (!changes) return false; // user cancelled → no-op
Object.assign(field, changes); // e.g. update value / options / reason
return true; // library redraws
},
});
onESignSignRequestedReplaces the entire sign pipeline. The library hands you the active
ESignDocument (with every touched field, role
artwork, etc.) and a progress reporter you can use to update the Sign button
label. Resolve with a signed File and the library opens it in the same pane
the user signed from and clears the preparation
(view.setESignDocument(null)). Resolve with null to cancel without error.
import { PdfEditor, FieldType, type ESignDocument } from '@avanquest/pdf-web-viewer';
await PdfEditor({
license: 'YOUR_LICENSE_KEY',
basePath: '/assets',
container: document.getElementById('viewer'),
onESignSignRequested: async (doc: ESignDocument, onProgress) => {
onProgress?.(5);
/* Encode the preparation into whatever payload your backend wants.
* Only touched fields are signal — untouched ones are placeholders. */
const placements = doc.roles.flatMap((role) =>
role.fields
.filter((f) => f.touched)
.map((f) => ({
roleId: role.id,
fieldType: f.fieldType,
pageIndex: f.pageIndex,
rect: { left: f.left, top: f.top, width: f.width, height: f.height },
value: f.value,
signatureImageSrc:
f.fieldType === FieldType.Signature
? role.signature?.src
: f.fieldType === FieldType.Initials
? role.initials?.src
: f.fieldType === FieldType.Certify
? role.certify?.src
: f.fieldType === FieldType.DigitalSignature
? role.digitalSignature?.src
: undefined,
})),
);
onProgress?.(20);
const signed = await myCustomSignClient.send({ placements }); // your backend
onProgress?.(100);
return new File([signed], 'signed.pdf', { type: 'application/pdf' });
/* Library: opens the file in the same pane + view.setESignDocument(null). */
},
});
Pre-flight validation still runs. The toolbar enforces
isRequired+touchedand blocks the sign action with a warning beforeonESignSignRequestedis ever called — so when your callback fires the preparation is already valid.
You don't have to use the built-in Sign action. The
index.IDocumentViewModel pair getESignDocument() / setESignDocument()
(plus eSignDocumentChanged())
is sufficient on its own to:
touched
flag and stores its value),ESignDocument at any time and hand it to your
own sign service — REST, gRPC, DocuSign, Adobe Sign, an in-house HSM
workflow, anything.The library never forces the built-in sign API path. Hide the toolbar's Sign button (or just ignore it), wire your own button to your own service, and use the viewer purely as a renderer + input collector.
import { PdfEditor, FieldType, type ESignDocument, type ESignField } from '@avanquest/pdf-web-viewer';
const editor = await PdfEditor({
license: 'YOUR_LICENSE_KEY',
basePath: '/assets',
container: document.getElementById('viewer'),
});
await editor.ui.pdfWebService.openDocument(myPdfFile);
const view = editor.ui.pdfWebService.getActiveDocumentViewElement().documentView;
/* 1. Render: hand the viewer a prepared ESignDocument (or start empty and let
* the user place fields via the toolbar). */
view.setESignDocument(
{
roles: [
/* ... */
],
},
{ security: true, fileType: true },
);
/* 2. Collect: subscribe to react to user edits (enable/disable your own
* "Send for signing" button, show progress, etc.). */
view.eSignDocumentChanged().subscribe((doc) => {
if (!doc) return;
const allRequiredFilled = doc.roles.flatMap((r) => r.fields).every((f) => !f.isRequired || f.touched);
myExternalSignButton.disabled = !allRequiredFilled;
});
/* 3. Read & send: when your own button is clicked, read the current state
* and post it to YOUR backend. Nothing else is required. */
async function sendToMySignService() {
const doc = view.getESignDocument();
if (!doc) return;
/* The original PDF the user is looking at — untouched by the viewer. */
const pdfBytes = await editor.ui.pdfWebService.getActiveDocumentViewElement().exportFile('uint8array');
/* Project the ESignDocument into whatever payload your service wants.
* Untouched placeholders are easy to drop here. */
const payload = {
pdf: pdfBytes,
placements: doc.roles.flatMap((role) =>
role.fields
.filter((f) => f.touched)
.map((f) => ({
roleId: role.id,
fieldType: f.fieldType,
pageIndex: f.pageIndex,
rect: { left: f.left, top: f.top, width: f.width, height: f.height },
value: f.value,
signatureImageSrc:
f.fieldType === FieldType.Signature
? role.signature?.src
: f.fieldType === FieldType.Initials
? role.initials?.src
: f.fieldType === FieldType.Certify
? role.certify?.src
: f.fieldType === FieldType.DigitalSignature
? role.digitalSignature?.src
: undefined,
})),
),
};
const signedPdf = await myCustomSignClient.send(payload);
/* 4. Done: clear the preparation and (optionally) open the signed file
* your service returned. The viewer was never coupled to the signer. */
view.setESignDocument(null);
await editor.ui.pdfWebService.openDocument(new File([signedPdf], 'signed.pdf', { type: 'application/pdf' }));
}
| Concern | Library provides | You implement |
|---|---|---|
| Rendering placeholder outlines on the PDF | ✅ | |
| Drag-to-place / resize / move field rects | ✅ | |
| Role colors, name / email / company profile fields | ✅ | |
| Per-field editors (text, dropdown, checkbox, radio, date) | ✅ | |
touched lifecycle (placeholder vs. filled) |
✅ | |
Required-field validation against isRequired |
✅ | |
| Eligibility gates on the host PDF (security/file-type/size) | ✅ | |
Reading the in-progress ESignDocument |
✅ | |
Encoding ESignDocument to YOUR service's request schema |
✅ | |
| Talking to YOUR signing backend / DocuSign / Adobe Sign | ✅ | |
| Producing the signed PDF / certifying / CMS / PKCS#7 | ✅ | |
| Opening the signed PDF back in the viewer | ✅ (openDocument) |
In other words: the built-in Sign action (described in
What happens at sign time) is one consumer of
this API. You can ignore it entirely and treat the viewer as a UI-only layer
that hands you a fully-typed ESignDocument to feed into your own signing
pipeline.