// ES6 Compliant Syntax
// Copilot: GitHub Copilot, Version: 1.0, Model: GPT-4, Version: 2024-06, Date: 2025-08-31
// File: client/index.js
// **Important**
// You must get a license key from Apryse to run the WebViewer Server SDK.
// A trial key can be obtained from:
// https://docs.apryse.com/core/guides/get-started/trial-key
const licenseKey = 'YOUR_WEBVIEWER_LICENSE_KEY';
const viewerElement = document.getElementById('viewer');
const initialDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/form_fields_flattened.pdf';
let jsonData = null;
const formFieldMap = [
{ type: 'formTextField', title: 'Text Field', color: { R: 51, G: 101, B: 251, A: 1 } },
{ type: 'formCheckBox', title: 'Check Box', color: { R: 0, G: 128, B: 0, A: 1 } },
{ type: 'formRadioButton', title: 'Radio Button', color: { R: 128, G: 0, B: 128, A: 1 } },
{ type: 'formListBox', title: 'List Box', color: { R: 144, G: 238, B: 144, A: 1 } },
{ type: 'formComboBox', title: 'Combo Box', color: { R: 255, G: 192, B: 203, A: 1 } },
{ type: 'formButton', title: 'Button', color: { R: 255, G: 165, B: 0, A: 1 } },
{ type: 'formDigitalSignature', title: 'Digital Signature', color: { R: 3, G: 219, B: 252, A: 1 } },
{ type: 'formTextArea', title: 'Text Area', color: { R: 255, G: 0, B: 0, A: 1 } },
];
// The list of registered panels in the main webviewer
let viewerPanels = null;
// The tab panel, representing the webviewer left panel
const tabPanel = {
handle: null,
dataElement: 'tabPanel'
};
// The custom form sub-panel to be registered
const formPanel = {
handle: null,
dataElement: 'formPanel',
render: null,
};
// Customize the main webviewer left panel after the load completion
const customizeUI = (instance) => {
const { UI } = instance;
// Close the tab panel (if it's open) for refreshment.
UI.closeElements([tabPanel.dataElement]);
// Get the list of registered panels in the main webviewer
viewerPanels = UI.getPanels();
// Find the Tab Panel to modify. The form sub-panel will be added to this Tab panel.
tabPanel.handle = viewerPanels.find((panel) => panel.dataElement === tabPanel.dataElement);
// Register the custom form sub-panel
RegisterFormPanel(instance);
// Add the new custom form sub-panel to list of sub-panels under the Tab Panel
formPanel.handle = { render: formPanel.dataElement };
tabPanel.handle.panelsList = [formPanel.handle, ...tabPanel.handle.panelsList];
UI.openElements([tabPanel.dataElement]);
UI.setPanelWidth(tabPanel.dataElement, 400);
};
// Register the custom form sub-panel
const RegisterFormPanel = (instance) => {
formPanel.render = createFormPanelElements(instance);
instance.UI.addPanel({
dataElement: formPanel.dataElement,
location: 'left',
icon: '<svg width="18px" height="18px" viewBox="0 0 24 24" id="圖層_1" data-name="圖層 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#080808;}</style></defs><title>form</title><path class="cls-1" d="M21,.5H3a2,2,0,0,0-2,2V22a2,2,0,0,0,2,2H21a2,2,0,0,0,2-2V2.5A2,2,0,0,0,21,.5Zm0,2v2H3v-2ZM3,22V6.5H21V22Z"/><path class="cls-1" d="M12.5,4H20a.5.5,0,0,0,0-1H12.5a.5.5,0,0,0,0,1Z"/><path class="cls-1" d="M4.5,4a.43.43,0,0,0,.19,0,.35.35,0,0,0,.16-.11A.47.47,0,0,0,5,3.5a.43.43,0,0,0,0-.19.36.36,0,0,0-.11-.16.5.5,0,0,0-.7,0A.35.35,0,0,0,4,3.31.43.43,0,0,0,4,3.5a.51.51,0,0,0,.5.5Z"/><path class="cls-1" d="M5.65,3.85A.36.36,0,0,0,5.81,4,.44.44,0,0,0,6,4a.47.47,0,0,0,.35-.15.36.36,0,0,0,.11-.16.6.6,0,0,0,0-.19.51.51,0,0,0-.15-.35A.49.49,0,0,0,5.81,3a.36.36,0,0,0-.16.11.47.47,0,0,0-.15.35.4.4,0,0,0,0,.19A.35.35,0,0,0,5.65,3.85Z"/><path class="cls-1" d="M8,8H4.5a1,1,0,0,0,0,2H8A1,1,0,0,0,8,8Z"/><path class="cls-1" d="M8,11.67H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M8,15.33H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M8,19H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,8H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,11.67H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,15.33H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,19H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,8h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,11.67h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,15.33h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,19h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/></svg>',
title: 'Form',
render: () => formPanel.render,
});
};
// Create the form panel elements.
const createFormPanelElements = (instance) => {
let panelDiv = document.createElement('div');
panelDiv.id = 'form';
let paragraph = document.createTextNode('A demo of Apryse SDK Smart Data Extraction and the form field detection and classification. Take a flat PDF and automatically find form fields using AI.');
panelDiv.appendChild(paragraph);
const span = document.createElement("span");
span.style.color = 'orange';
span.appendChild(document.createTextNode('NOTE: Only the first page will be processed.'));
panelDiv.appendChild(document.createElement('p'));
panelDiv.appendChild(span);
let dividerDiv = document.createElement('div');
dividerDiv.style.borderTop = '1px solid #ccc';
dividerDiv.style.margin = '10px 0';
panelDiv.appendChild(dividerDiv);
// Detect form fields button
let detectFieldsButton = document.createElement('button');
detectFieldsButton.textContent = 'Detect Form Fields';
detectFieldsButton.id = 'detectFieldsButton';
detectFieldsButton.onclick = async () => {
detectFieldsButton.style.cursor = "not-allowed"; // Changes cursor for the button itself
formPanel.render.style.cursor = "not-allowed"; // Changes cursor for the button itself
enableButton(detectFieldsButton, false);
await detectFormFields(instance); // Detect form fields
detectFieldsButton.style.cursor = "default";
formPanel.render.style.cursor = "default";
}
enableButton(detectFieldsButton, false); // Disabled until a document is loaded
panelDiv.appendChild(detectFieldsButton);
panelDiv.appendChild(document.createElement('p'));
return panelDiv;
};
// Open JSON data in a dialog box with zoom in/out and close buttons
const openJsonDataDialog = (jsonText) => {
let fontSize = 14;
// Create overlay
const overlay = document.createElement("div");
overlay.className = "modal-overlay";
overlay.onclick = (e) => {
if (e.target === overlay) {
document.body.removeChild(overlay);
}
};
// Modal box
const modal = document.createElement("div");
modal.className = "modal-box";
// Controls
const controls = document.createElement("div");
controls.className = "modal-controls";
const zoomInBtn = document.createElement("button");
zoomInBtn.textContent = "+";
zoomInBtn.onclick = () => {
fontSize += 2;
content.style.fontSize = fontSize + "px";
};
const zoomOutBtn = document.createElement("button");
zoomOutBtn.textContent = "-";
zoomOutBtn.onclick = () => {
fontSize = Math.max(10, fontSize - 2);
content.style.fontSize = fontSize + "px";
};
const closeBtn = document.createElement("button");
closeBtn.textContent = "Close";
closeBtn.className = "modal-close";
closeBtn.onclick = () => {
document.body.removeChild(overlay);
};
controls.appendChild(zoomInBtn);
controls.appendChild(zoomOutBtn);
controls.appendChild(closeBtn);
// Content
const content = document.createElement("pre");
content.className = "modal-content";
content.style.fontSize = fontSize + "px";
content.innerHTML = jsonText;
modal.appendChild(controls);
modal.appendChild(content);
overlay.appendChild(modal);
document.body.appendChild(overlay);
}
// Draw annotation rectangles on the PDF when forms fields are detected
const drawAnnotations = (instance) => {
const formFieldData = JSON.parse(jsonData);
const { annotationManager, Annotations } = instance.Core;
formFieldData.pages[0].formElements.forEach((element) => {
const color = formFieldMap.find(field => field.type === element.type).color;
const annot = new Annotations.RectangleAnnotation({
PageNumber: formFieldData.pages[0].properties.pageNumber,
X: element.rect[0],
Y: element.rect[1],
Width: element.rect[2] - element.rect[0],
Height: element.rect[3] - element.rect[1],
StrokeColor: new Annotations.Color(color.R, color.G, color.B, color.A),
StrokeThickness: 2,
});
annotationManager.addAnnotation(annot);
annotationManager.redrawAnnotation(annot);
});
};
const downloadPdf = async (instance) => {
const options = {
flags: instance.Core.SaveOptions.LINEARIZED,
downloadType: 'pdf'
};
instance.UI.downloadPdf(options);
};
// Build form fields and draw annotation rectangles
// on the PDF when the "Build Form" button is clicked
const buildForm = (instance) => {
const { annotationManager, Annotations } = instance.Core;
const { WidgetFlags } = Annotations;
let flags = null;
let field = null;
let widgetAnnot = null;
const font = new Annotations.Font({ name: 'Helvetica', size: 12 });
annotationManager.deleteAnnotations(annotationManager.getAnnotationsList());
const formFieldData = JSON.parse(jsonData);
formFieldData.pages[0].formElements.forEach((formField, index) => {
// Sets generic flags for the widget.
flags = new WidgetFlags();
flags.set(WidgetFlags.REQUIRED, true);
switch (formField.type) {
// Text field
case 'formTextField':
flags.set(WidgetFlags.MULTILINE, true);
// Creates a text form field.
field = new Annotations.Forms.Field(`TextField ${index}`, {
type: 'Tx',
defaultValue: 'Default Value',
flags,
});
// Creates a text widget annotation.
widgetAnnot = new Annotations.TextWidgetAnnotation(field);
break;
// Radio button
case 'formRadioButton':
flags.set(WidgetFlags.RADIO, true);
flags.set(WidgetFlags.NO_TOGGLE_TO_OFF, true);
// Creates a radio button form field.
field = new Annotations.Forms.Field(`RadioField ${index}`, {
type: 'Btn',
value: 'Off',
flags,
font: font,
});
// Create a radio widget button.
widgetAnnot = new Annotations.RadioButtonWidgetAnnotation(field, {
appearance: 'Off',
appearances: {
Off: {},
First: {},
},
backgroundColor: new Color(255, 0, 0),
});
break;
// List box
case 'formComboBox':
// Sets flags for the combobox widget.
flags.set(WidgetFlags.COMBO, true);
// Define the available options.
const comboOptions = [
{ value: '1', displayValue: 'one' },
{ value: '2', displayValue: 'two' },
{ value: '3', displayValue: 'three' }
];
// Creates a combobox form field.
field = new Annotations.Forms.Field(`ComboBoxField ${index}`, {
flags,
font: font,
type: 'Ch',
options: comboOptions,
value: comboOptions[0].value,
});
// Creates a combobox widget annotation.
widgetAnnot = new Annotations.ChoiceWidgetAnnotation(field);
break;
// Check box
case 'formCheckBox':
// Creates a checkbox form field.
field = new Annotations.Forms.Field(`CheckBoxField ${index}`, {
type: 'Btn',
value: 'Off',
flags,
});
// Creates a checkbox widget annotation.
widgetAnnot = new Annotations.CheckButtonWidgetAnnotation(field, {
appearance: 'Off',
appearances: {
Off: {},
Yes: {},
},
captions: {
Normal: '' // Uses the check symbol for selected caption.
}
});
break;
// Digital signature
case 'formDigitalSignature':
// Creates a signature form field.
field = new Annotations.Forms.Field(`SignatureField ${index}`, {
type: 'Sig',
flags,
});
// Creates a signature widget annotation.
widgetAnnot = new Annotations.SignatureWidgetAnnotation(field, {
appearance: '_DEFAULT',
appearances: {
_DEFAULT: {
Normal: {
// Optionally can pass image data to appearance.
// data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjEuMWMqnEsAAAANSURBVBhXY/j//z8DAAj8Av6IXwbgAAAAAElFTkSuQmCC',
offset: {
x: formField.rect[0],
y: formField.rect[1],
},
},
},
},
});
break;
// Button
case 'formButton':
flags.set(WidgetFlags.PUSH_BUTTON, true);
// Creates a button form field.
field = new Annotations.Forms.Field(`ButtonField ${index}`, {
type: 'Btn',
tooltipName: 'this is a button',
flags,
});
// Creates a checkbox widget annotation.
widgetAnnot = new Annotations.PushButtonWidgetAnnotation(field, {
border: new Annotations.Border({
color: new Annotations.Color(255, 0, 0),
width: 1,
style: 'solid',
}),
});
break;
default:
break;
}
// set the widget properties
widgetAnnot.PageNumber = formFieldData.pages[0].properties.pageNumber;
widgetAnnot.X = formField.rect[0];
widgetAnnot.Y = formField.rect[1];
widgetAnnot.Width = formField.rect[2] - formField.rect[0];
widgetAnnot.Height = formField.rect[3] - formField.rect[1];
// Add form field to field manager and widget annotation to annotation manager.
annotationManager.getFieldManager().addField(field);
annotationManager.addAnnotation(widgetAnnot);
annotationManager.drawAnnotationsFromList([widgetAnnot]);
});
}
// Detect form fields in the PDF document
// This function will send GET message to the server,
// to receive the detected form fields as JSON object.
const detectFormFields = async (instance) => {
const doc = instance.Core.documentViewer.getDocument();
// Make a GET request to get the extracted JSON data of form fields of the current PDF.
return new Promise(function (resolve) {
fetch(`http://localhost:5050/server/handler.js?filename=${doc.filename}`, {
method: 'GET'
}).then(function (response) {
if (response.status === 200) {
response.text().then(function (json) {
jsonData = json;
let jsonText = JSON.stringify(jsonData, null, 2);
jsonText = jsonText.replace(/\\r\\n/g, '\n');
jsonText = jsonText.replace(/\\"/g, '"');
// Build form button
let buildFormButton = document.createElement('button');
buildFormButton.textContent = 'Build Form';
buildFormButton.id = 'buildFormButton';
buildFormButton.onclick = () => {
enableButton(buildFormButton, false);
buildForm(instance); // Build form
}
enableButton(buildFormButton, false);
formPanel.render.appendChild(buildFormButton);
formPanel.render.appendChild(document.createElement('p'));
// Download button
let downloadButton = document.createElement('button');
downloadButton.textContent = 'Download PDF';
downloadButton.id = 'downloadButton';
downloadButton.style.backgroundColor = 'blue';
downloadButton.style.color = 'white';
downloadButton.onclick = () => downloadPdf(instance); // Download PDF
formPanel.render.appendChild(downloadButton);
// Display the detected form fields
let colorsDiv = document.createElement('div');
colorsDiv.id = 'json';
colorsDiv.className = "listContainer";
const colorsTitle = document.createElement("h3");
colorsTitle.textContent = "Color Legend";
colorsDiv.appendChild(colorsTitle);
colorsDiv.appendChild(document.createElement('p'));
// Create list items
formFieldMap.forEach(field => {
const color = new instance.Core.Annotations.Color(field.color.R, field.color.G, field.color.B, field.color.A);
const listItem = document.createElement("div");
listItem.className = "listItem";
listItem.textContent = field.text;
listItem.style.setProperty("--bullet-color", color);
listItem.style.setProperty("color", color);
listItem.style.setProperty("font-weight", "bold");
// Set bullet color using ::before
listItem.style.setProperty("--bullet-color", color);
listItem.style.setProperty("position", "relative");
listItem.style.setProperty("padding-left", "20px");
listItem.style.setProperty("margin", "8px 0");
// Add custom bullet using inline style
listItem.style.setProperty("list-style", "none");
listItem.style.setProperty("display", "block");
listItem.style.setProperty("line-height", "1.5");
listItem.style.setProperty("font-size", "14px");
// Create bullet manually
const bullet = document.createElement("span");
bullet.style.width = "10px";
bullet.style.height = "10px";
bullet.style.borderRadius = "50%";
bullet.style.backgroundColor = color;
bullet.style.display = "inline-block";
bullet.style.marginRight = "10px";
bullet.style.verticalAlign = "middle";
// Insert bullet before text
listItem.textContent = ""; // Clear text
listItem.appendChild(bullet);
listItem.appendChild(document.createTextNode(field.title));
colorsDiv.appendChild(listItem);
});
formPanel.render.appendChild(colorsDiv);
// Display the detected form fields
let jsonDiv = document.createElement('div');
jsonDiv.id = 'json';
const jsonTitle = document.createElement("h3");
jsonTitle.textContent = "JSON Data";
jsonDiv.appendChild(jsonTitle);
jsonDiv.appendChild(document.createElement('p'));
const scrollBox = document.createElement("div");
scrollBox.style.width = "350px";
scrollBox.style.height = "350px";
scrollBox.style.border = "2px solid #444";
scrollBox.style.overflow = "scroll"; // Enables both vertical and horizontal scroll
scrollBox.style.whiteSpace = "nowrap"; // Prevents wrapping for horizontal scroll
scrollBox.style.padding = "10px";
scrollBox.style.fontFamily = "monospace";
scrollBox.style.backgroundColor = "black";
scrollBox.style.color = "white";
// Format and insert JSON data
const jsonContent = document.createElement("pre");
jsonContent.textContent = jsonText;
scrollBox.appendChild(jsonContent);
jsonDiv.appendChild(scrollBox);
// Open JSON data dialog button
let jsonDataDialogButton = document.createElement('button');
jsonDataDialogButton.textContent = 'Open in Dialog';
jsonDataDialogButton.id = 'jsonDataDialogButton';
jsonDataDialogButton.style.backgroundColor = 'blue';
jsonDataDialogButton.style.color = 'white';
jsonDataDialogButton.onclick = () => openJsonDataDialog(jsonText);
jsonDiv.appendChild(jsonDataDialogButton);
jsonDiv.appendChild(document.createElement('p'));
formPanel.render.appendChild(jsonDiv);
drawAnnotations(instance);
resolve();
enableButton(formPanel.render.querySelector('#buildFormButton'), true);
})
}
else if (response.status === 500) {
jsonData = null;
resolve();
}
});
});
};
// Enable or disable a button based on the state
const enableButton = (button, state) => {
button.disabled = !state;
button.style.backgroundColor = (state) ? 'blue' : 'gray';
button.style.color = (state) ? 'white' : 'darkgray';
};
WebViewer({
path: '/lib',
initialDoc: initialDoc,
enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
enableMeasurement: true,
loadAsPDF: true,
licenseKey: licenseKey,
}, viewerElement).then(instance => {
// Once the PDF document is loaded, send it to the server.
// The sent PDF document will be processed by the server,
// by extracting form fields JSON data when the user clicks the "Detect Form Fields" button.
instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
// Customize the main webviewer left panel after the load completion
customizeUI(instance);
// Reset JSON data
jsonData = null;
// Preparation of the PDF blob to be sent to the server
const doc = instance.Core.documentViewer.getDocument();
const xfdfString = await instance.Core.annotationManager.exportAnnotations(); // obtaining annotations in the loaded document
const data = await doc.getFileData({ xfdfString });
const arr = new Uint8Array(data);
const blob = new Blob([arr], { type: 'application/pdf' });
const formData = new FormData();
formData.append(doc.filename, blob, doc.filename);
// Send the PDF blob to the server for processing
new Promise(function (resolve, reject) {
console.log('🚀 Sending PDF to server for initial processing...');
fetch(`http://localhost:5050/server/handler.js?filename=${doc.filename}`, {
method: 'POST',
body: formData,
}).then(function (response) {
console.log(`📡 Server response status: ${response.status}`);
if (response.status === 200) {
console.log('✅ PDF successfully sent to server');
// enable Detect Form Fields button
const detectButton = formPanel.render.querySelector('#detectFieldsButton');
if (detectButton) {
console.log('🔓 Enabling Detect Form Fields button');
enableButton(detectButton, true);
} else {
console.warn('⚠️ Could not find detectFieldsButton in DOM');
}
resolve();
} else {
console.error(`❌ Server responded with status: ${response.status}`);
reject(new Error(`Server error: ${response.status}`));
}
}).catch(function (error) {
console.error('❌ Failed to connect to server:', error);
console.error('📍 Attempted URL: http://localhost:5050/server/handler.js');
console.error('🔍 This likely means the field-detection server is not running on port 5050');
reject(error);
});
}).catch(function (error) {
console.error('❌ Error in PDF upload promise:', error);
});
});
console.log('✅ WebViewer loaded successfully.');
}).catch((error) => {
console.error('❌ Failed to initialize WebViewer:', error);
});