Effortlessly detect and classify Form Fields. Transform your flat PDF into an interactive form with editable fields using AI-powered recognition.
This demo allows you to:
Implementation steps
To add PDF form field detection capability with WebViewer:
Step 1: Choose your preferred web stack
Step 2: Download the required modules listed in the Demo Dependencies section below
Step 3: Add the ES6 JavaScript sample code provided in this guide
Demo Dependencies
This sample uses the following:
1// ES6 Compliant Syntax
2// Copilot: GitHub Copilot, Version: 1.0, Model: GPT-4, Version: 2024-06, Date: 2025-08-31
3// File: client/index.js
4
5
6// **Important**
7// You must get a license key from Apryse to run the WebViewer Server SDK.
8// A trial key can be obtained from:
9// https://docs.apryse.com/core/guides/get-started/trial-key
10const licenseKey = 'YOUR_LICENSE_KEY';
11const viewerElement = document.getElementById('viewer');
12const initialDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/form_fields_flattened.pdf';
13let jsonData = null;
14const formFieldMap = [
15 { type: 'formTextField', title: 'Text Field', color: { R: 51, G: 101, B: 251, A: 1 } },
16 { type: 'formCheckBox', title: 'Check Box', color: { R: 0, G: 128, B: 0, A: 1 } },
17 { type: 'formRadioButton', title: 'Radio Button', color: { R: 128, G: 0, B: 128, A: 1 } },
18 { type: 'formListBox', title: 'List Box', color: { R: 144, G: 238, B: 144, A: 1 } },
19 { type: 'formComboBox', title: 'Combo Box', color: { R: 255, G: 192, B: 203, A: 1 } },
20 { type: 'formButton', title: 'Button', color: { R: 255, G: 165, B: 0, A: 1 } },
21 { type: 'formDigitalSignature', title: 'Digital Signature', color: { R: 3, G: 219, B: 252, A: 1 } },
22 { type: 'formTextArea', title: 'Text Area', color: { R: 255, G: 0, B: 0, A: 1 } },
23];
24
25// The list of registered panels in the main webviewer
26let viewerPanels = null;
27
28// The tab panel, representing the webviewer left panel
29const tabPanel = {
30 handle: null,
31 dataElement: 'tabPanel'
32};
33
34// The custom form sub-panel to be registered
35const formPanel = {
36 handle: null,
37 dataElement: 'formPanel',
38 render: null,
39};
40
41// Customize the main webviewer left panel after the load completion
42const customizeUI = (instance) => {
43 const { UI } = instance;
44
45 // Close the tab panel (if it's open) for refreshment.
46 UI.closeElements([tabPanel.dataElement]);
47
48 // Get the list of registered panels in the main webviewer
49 viewerPanels = UI.getPanels();
50
51 // Find the Tab Panel to modify. The form sub-panel will be added to this Tab panel.
52 tabPanel.handle = viewerPanels.find((panel) => panel.dataElement === tabPanel.dataElement);
53
54 // Register the custom form sub-panel
55 RegisterFormPanel(instance);
56
57 // Add the new custom form sub-panel to list of sub-panels under the Tab Panel
58 formPanel.handle = { render: formPanel.dataElement };
59 tabPanel.handle.panelsList = [formPanel.handle, ...tabPanel.handle.panelsList];
60
61 UI.openElements([tabPanel.dataElement]);
62 UI.setPanelWidth(tabPanel.dataElement, 400);
63};
64
65// Register the custom form sub-panel
66const RegisterFormPanel = (instance) => {
67 formPanel.render = createFormPanelElements(instance);
68 instance.UI.addPanel({
69 dataElement: formPanel.dataElement,
70 location: 'left',
71 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>',
72 title: 'Form',
73 render: () => formPanel.render,
74 });
75};
76
77// Create the form panel elements.
78const createFormPanelElements = (instance) => {
79 let panelDiv = document.createElement('div');
80 panelDiv.id = 'form';
81 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.');
82 panelDiv.appendChild(paragraph);
83
84 const span = document.createElement("span");
85 span.style.color = 'orange';
86 span.appendChild(document.createTextNode('NOTE: Only the first page will be processed.'));
87 panelDiv.appendChild(document.createElement('p'));
88 panelDiv.appendChild(span);
89
90 let dividerDiv = document.createElement('div');
91 dividerDiv.style.borderTop = '1px solid #ccc';
92 dividerDiv.style.margin = '10px 0';
93 panelDiv.appendChild(dividerDiv);
94
95 // Detect form fields button
96 let detectFieldsButton = document.createElement('button');
97 detectFieldsButton.textContent = 'Detect Form Fields';
98 detectFieldsButton.id = 'detectFieldsButton';
99 detectFieldsButton.onclick = async () => {
100
101 detectFieldsButton.style.cursor = "not-allowed"; // Changes cursor for the button itself
102 formPanel.render.style.cursor = "not-allowed"; // Changes cursor for the button itself
103
104 enableButton(detectFieldsButton, false);
105 await detectFormFields(instance); // Detect form fields
106
107 detectFieldsButton.style.cursor = "default";
108 formPanel.render.style.cursor = "default";
109 }
110 enableButton(detectFieldsButton, false); // Disabled until a document is loaded
111
112 panelDiv.appendChild(detectFieldsButton);
113 panelDiv.appendChild(document.createElement('p'));
114
115 return panelDiv;
116};
117
118// Open JSON data in a dialog box with zoom in/out and close buttons
119const openJsonDataDialog = (jsonText) => {
120 let fontSize = 14;
121
122 // Create overlay
123 const overlay = document.createElement("div");
124 overlay.className = "modal-overlay";
125 overlay.onclick = (e) => {
126 if (e.target === overlay) {
127 document.body.removeChild(overlay);
128 }
129 };
130
131 // Modal box
132 const modal = document.createElement("div");
133 modal.className = "modal-box";
134
135 // Controls
136 const controls = document.createElement("div");
137 controls.className = "modal-controls";
138
139 const zoomInBtn = document.createElement("button");
140 zoomInBtn.textContent = "+";
141 zoomInBtn.onclick = () => {
142 fontSize += 2;
143 content.style.fontSize = fontSize + "px";
144 };
145
146 const zoomOutBtn = document.createElement("button");
147 zoomOutBtn.textContent = "-";
148 zoomOutBtn.onclick = () => {
149 fontSize = Math.max(10, fontSize - 2);
150 content.style.fontSize = fontSize + "px";
151 };
152
153 const closeBtn = document.createElement("button");
154 closeBtn.textContent = "Close";
155 closeBtn.className = "modal-close";
156 closeBtn.onclick = () => {
157 document.body.removeChild(overlay);
158 };
159
160 controls.appendChild(zoomInBtn);
161 controls.appendChild(zoomOutBtn);
162 controls.appendChild(closeBtn);
163
164 // Content
165 const content = document.createElement("pre");
166 content.className = "modal-content";
167 content.style.fontSize = fontSize + "px";
168 content.innerHTML = jsonText;
169
170 modal.appendChild(controls);
171 modal.appendChild(content);
172 overlay.appendChild(modal);
173 document.body.appendChild(overlay);
174}
175
176// Draw annotation rectangles on the PDF when forms fields are detected
177const drawAnnotations = (instance) => {
178 const formFieldData = JSON.parse(jsonData);
179 const { annotationManager, Annotations } = instance.Core;
180
181 formFieldData.pages[0].formElements.forEach((element) => {
182 const color = formFieldMap.find(field => field.type === element.type).color;
183 const annot = new Annotations.RectangleAnnotation({
184 PageNumber: formFieldData.pages[0].properties.pageNumber,
185 X: element.rect[0],
186 Y: element.rect[1],
187 Width: element.rect[2] - element.rect[0],
188 Height: element.rect[3] - element.rect[1],
189 StrokeColor: new Annotations.Color(color.R, color.G, color.B, color.A),
190 StrokeThickness: 2,
191 });
192 annotationManager.addAnnotation(annot);
193 annotationManager.redrawAnnotation(annot);
194 });
195};
196
197const downloadPdf = async (instance) => {
198 const options = {
199 flags: instance.Core.SaveOptions.LINEARIZED,
200 downloadType: 'pdf'
201 };
202
203 instance.UI.downloadPdf(options);
204};
205
206// Build form fields and draw annotation rectangles
207// on the PDF when the "Build Form" button is clicked
208const buildForm = (instance) => {
209 const { annotationManager, Annotations } = instance.Core;
210 const { WidgetFlags } = Annotations;
211 let flags = null;
212 let field = null;
213 let widgetAnnot = null;
214 const font = new Annotations.Font({ name: 'Helvetica', size: 12 });
215
216 annotationManager.deleteAnnotations(annotationManager.getAnnotationsList());
217
218 const formFieldData = JSON.parse(jsonData);
219
220 formFieldData.pages[0].formElements.forEach((formField, index) => {
221
222 // Sets generic flags for the widget.
223 flags = new WidgetFlags();
224 flags.set(WidgetFlags.REQUIRED, true);
225
226 switch (formField.type) {
227 // Text field
228 case 'formTextField':
229 flags.set(WidgetFlags.MULTILINE, true);
230
231 // Creates a text form field.
232 field = new Annotations.Forms.Field(`TextField ${index}`, {
233 type: 'Tx',
234 defaultValue: 'Default Value',
235 flags,
236 });
237
238 // Creates a text widget annotation.
239 widgetAnnot = new Annotations.TextWidgetAnnotation(field);
240 break;
241
242 // Radio button
243 case 'formRadioButton':
244 flags.set(WidgetFlags.RADIO, true);
245 flags.set(WidgetFlags.NO_TOGGLE_TO_OFF, true);
246
247 // Creates a radio button form field.
248 field = new Annotations.Forms.Field(`RadioField ${index}`, {
249 type: 'Btn',
250 value: 'Off',
251 flags,
252 font: font,
253 });
254
255 // Create a radio widget button.
256 widgetAnnot = new Annotations.RadioButtonWidgetAnnotation(field, {
257 appearance: 'Off',
258 appearances: {
259 Off: {},
260 First: {},
261 },
262 backgroundColor: new Color(255, 0, 0),
263 });
264 break;
265
266 // List box
267 case 'formComboBox':
268 // Sets flags for the combobox widget.
269 flags.set(WidgetFlags.COMBO, true);
270
271 // Define the available options.
272 const comboOptions = [
273 { value: '1', displayValue: 'one' },
274 { value: '2', displayValue: 'two' },
275 { value: '3', displayValue: 'three' }
276 ];
277
278 // Creates a combobox form field.
279 field = new Annotations.Forms.Field(`ComboBoxField ${index}`, {
280 flags,
281 font: font,
282 type: 'Ch',
283 options: comboOptions,
284 value: comboOptions[0].value,
285 });
286
287 // Creates a combobox widget annotation.
288 widgetAnnot = new Annotations.ChoiceWidgetAnnotation(field);
289 break;
290
291 // Check box
292 case 'formCheckBox':
293 // Creates a checkbox form field.
294 field = new Annotations.Forms.Field(`CheckBoxField ${index}`, {
295 type: 'Btn',
296 value: 'Off',
297 flags,
298 });
299
300 // Creates a checkbox widget annotation.
301 widgetAnnot = new Annotations.CheckButtonWidgetAnnotation(field, {
302 appearance: 'Off',
303 appearances: {
304 Off: {},
305 Yes: {},
306 },
307 captions: {
308 Normal: '' // Uses the check symbol for selected caption.
309 }
310 });
311 break;
312
313 // Digital signature
314 case 'formDigitalSignature':
315 // Creates a signature form field.
316 field = new Annotations.Forms.Field(`SignatureField ${index}`, {
317 type: 'Sig',
318 flags,
319 });
320
321 // Creates a signature widget annotation.
322 widgetAnnot = new Annotations.SignatureWidgetAnnotation(field, {
323 appearance: '_DEFAULT',
324 appearances: {
325 _DEFAULT: {
326 Normal: {
327 // Optionally can pass image data to appearance.
328 // data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjEuMWMqnEsAAAANSURBVBhXY/j//z8DAAj8Av6IXwbgAAAAAElFTkSuQmCC',
329 offset: {
330 x: formField.rect[0],
331 y: formField.rect[1],
332 },
333 },
334 },
335 },
336 });
337 break;
338
339 // Button
340 case 'formButton':
341 flags.set(WidgetFlags.PUSH_BUTTON, true);
342 // Creates a button form field.
343 field = new Annotations.Forms.Field(`ButtonField ${index}`, {
344 type: 'Btn',
345 tooltipName: 'this is a button',
346 flags,
347 });
348
349 // Creates a checkbox widget annotation.
350 widgetAnnot = new Annotations.PushButtonWidgetAnnotation(field, {
351 border: new Annotations.Border({
352 color: new Annotations.Color(255, 0, 0),
353 width: 1,
354 style: 'solid',
355 }),
356 });
357 break;
358
359 default:
360 break;
361 }
362
363 // set the widget properties
364 widgetAnnot.PageNumber = formFieldData.pages[0].properties.pageNumber;
365 widgetAnnot.X = formField.rect[0];
366 widgetAnnot.Y = formField.rect[1];
367 widgetAnnot.Width = formField.rect[2] - formField.rect[0];
368 widgetAnnot.Height = formField.rect[3] - formField.rect[1];
369
370 // Add form field to field manager and widget annotation to annotation manager.
371 annotationManager.getFieldManager().addField(field);
372 annotationManager.addAnnotation(widgetAnnot);
373 annotationManager.drawAnnotationsFromList([widgetAnnot]);
374 });
375}
376
377// Detect form fields in the PDF document
378// This function will send GET message to the server,
379// to receive the detected form fields as JSON object.
380const detectFormFields = async (instance) => {
381
382 const doc = instance.Core.documentViewer.getDocument();
383
384 // Make a GET request to get the extracted JSON data of form fields of the current PDF.
385 return new Promise(function (resolve) {
386 fetch(`http://localhost:5050/server/handler.js?filename=${doc.filename}`, {
387 method: 'GET'
388 }).then(function (response) {
389 if (response.status === 200) {
390 response.text().then(function (json) {
391 jsonData = json;
392 let jsonText = JSON.stringify(jsonData, null, 2);
393 jsonText = jsonText.replace(/\\r\\n/g, '\n');
394 jsonText = jsonText.replace(/\\"/g, '"');
395
396 // Build form button
397 let buildFormButton = document.createElement('button');
398 buildFormButton.textContent = 'Build Form';
399 buildFormButton.id = 'buildFormButton';
400 buildFormButton.onclick = () => {
401 enableButton(buildFormButton, false);
402 buildForm(instance); // Build form
403 }
404 enableButton(buildFormButton, false);
405
406 formPanel.render.appendChild(buildFormButton);
407 formPanel.render.appendChild(document.createElement('p'));
408
409 // Download button
410 let downloadButton = document.createElement('button');
411 downloadButton.textContent = 'Download PDF';
412 downloadButton.id = 'downloadButton';
413 downloadButton.style.backgroundColor = 'blue';
414 downloadButton.style.color = 'white';
415 downloadButton.onclick = () => downloadPdf(instance); // Download PDF
416
417 formPanel.render.appendChild(downloadButton);
418
419 // Display the detected form fields
420 let colorsDiv = document.createElement('div');
421 colorsDiv.id = 'json';
422 colorsDiv.className = "listContainer";
423 const colorsTitle = document.createElement("h3");
424 colorsTitle.textContent = "Color Legend";
425 colorsDiv.appendChild(colorsTitle);
426 colorsDiv.appendChild(document.createElement('p'));
427
428 // Create list items
429 formFieldMap.forEach(field => {
430 const color = new instance.Core.Annotations.Color(field.color.R, field.color.G, field.color.B, field.color.A);
431 const listItem = document.createElement("div");
432 listItem.className = "listItem";
433 listItem.textContent = field.text;
434 listItem.style.setProperty("--bullet-color", color);
435 listItem.style.setProperty("color", color);
436 listItem.style.setProperty("font-weight", "bold");
437
438 // Set bullet color using ::before
439 listItem.style.setProperty("--bullet-color", color);
440 listItem.style.setProperty("position", "relative");
441 listItem.style.setProperty("padding-left", "20px");
442 listItem.style.setProperty("margin", "8px 0");
443
444 // Add custom bullet using inline style
445 listItem.style.setProperty("list-style", "none");
446 listItem.style.setProperty("display", "block");
447 listItem.style.setProperty("line-height", "1.5");
448 listItem.style.setProperty("font-size", "14px");
449
450 // Create bullet manually
451 const bullet = document.createElement("span");
452 bullet.style.width = "10px";
453 bullet.style.height = "10px";
454 bullet.style.borderRadius = "50%";
455 bullet.style.backgroundColor = color;
456 bullet.style.display = "inline-block";
457 bullet.style.marginRight = "10px";
458 bullet.style.verticalAlign = "middle";
459
460 // Insert bullet before text
461 listItem.textContent = ""; // Clear text
462 listItem.appendChild(bullet);
463 listItem.appendChild(document.createTextNode(field.title));
464
465 colorsDiv.appendChild(listItem);
466 });
467
468 formPanel.render.appendChild(colorsDiv);
469
470 // Display the detected form fields
471 let jsonDiv = document.createElement('div');
472 jsonDiv.id = 'json';
473 const jsonTitle = document.createElement("h3");
474 jsonTitle.textContent = "JSON Data";
475 jsonDiv.appendChild(jsonTitle);
476 jsonDiv.appendChild(document.createElement('p'));
477
478 const scrollBox = document.createElement("div");
479 scrollBox.style.width = "350px";
480 scrollBox.style.height = "350px";
481 scrollBox.style.border = "2px solid #444";
482 scrollBox.style.overflow = "scroll"; // Enables both vertical and horizontal scroll
483 scrollBox.style.whiteSpace = "nowrap"; // Prevents wrapping for horizontal scroll
484 scrollBox.style.padding = "10px";
485 scrollBox.style.fontFamily = "monospace";
486 scrollBox.style.backgroundColor = "black";
487 scrollBox.style.color = "white";
488
489 // Format and insert JSON data
490 const jsonContent = document.createElement("pre");
491 jsonContent.textContent = jsonText;
492 scrollBox.appendChild(jsonContent);
493 jsonDiv.appendChild(scrollBox);
494
495 // Open JSON data dialog button
496 let jsonDataDialogButton = document.createElement('button');
497 jsonDataDialogButton.textContent = 'Open in Dialog';
498 jsonDataDialogButton.id = 'jsonDataDialogButton';
499 jsonDataDialogButton.style.backgroundColor = 'blue';
500 jsonDataDialogButton.style.color = 'white';
501 jsonDataDialogButton.onclick = () => openJsonDataDialog(jsonText);
502 jsonDiv.appendChild(jsonDataDialogButton);
503 jsonDiv.appendChild(document.createElement('p'));
504
505 formPanel.render.appendChild(jsonDiv);
506 drawAnnotations(instance);
507 resolve();
508 enableButton(formPanel.render.querySelector('#buildFormButton'), true);
509 })
510 }
511 else if (response.status === 500) {
512 jsonData = null;
513 resolve();
514 }
515 });
516 });
517};
518
519// Enable or disable a button based on the state
520const enableButton = (button, state) => {
521 button.disabled = !state;
522 button.style.backgroundColor = (state) ? 'blue' : 'gray';
523 button.style.color = (state) ? 'white' : 'darkgray';
524};
525
526WebViewer({
527 path: '/lib',
528 initialDoc: initialDoc,
529 enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
530 enableMeasurement: true,
531 loadAsPDF: true,
532 licenseKey: licenseKey,
533}, viewerElement).then(instance => {
534
535 // Once the PDF document is loaded, send it to the server.
536 // The sent PDF document will be processed by the server,
537 // by extracting form fields JSON data when the user clicks the "Detect Form Fields" button.
538 instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
539
540 // Customize the main webviewer left panel after the load completion
541 customizeUI(instance);
542
543 // Reset JSON data
544 jsonData = null;
545
546 // Preparation of the PDF blob to be sent to the server
547 const doc = instance.Core.documentViewer.getDocument();
548 const xfdfString = await instance.Core.annotationManager.exportAnnotations(); // obtaining annotations in the loaded document
549 const data = await doc.getFileData({ xfdfString });
550 const arr = new Uint8Array(data);
551 const blob = new Blob([arr], { type: 'application/pdf' });
552 const formData = new FormData();
553 formData.append(doc.filename, blob, doc.filename);
554
555 // Send the PDF blob to the server for processing
556 new Promise(function (resolve, reject) {
557 console.log('🚀 Sending PDF to server for initial processing...');
558
559 fetch(`http://localhost:5050/server/handler.js?filename=${doc.filename}`, {
560 method: 'POST',
561 body: formData,
562 }).then(function (response) {
563 console.log(`📡 Server response status: ${response.status}`);
564
565 if (response.status === 200) {
566 console.log('✅ PDF successfully sent to server');
567
568 // enable Detect Form Fields button
569 const detectButton = formPanel.render.querySelector('#detectFieldsButton');
570 if (detectButton) {
571 console.log('🔓 Enabling Detect Form Fields button');
572 enableButton(detectButton, true);
573 } else {
574 console.warn('⚠️ Could not find detectFieldsButton in DOM');
575 }
576 resolve();
577 } else {
578 console.error(`❌ Server responded with status: ${response.status}`);
579 reject(new Error(`Server error: ${response.status}`));
580 }
581 }).catch(function (error) {
582 console.error('❌ Failed to connect to server:', error);
583 console.error('📍 Attempted URL: http://localhost:5050/server/handler.js');
584 console.error('🔍 This likely means the field-detection server is not running on port 5050');
585 reject(error);
586 });
587 }).catch(function (error) {
588 console.error('❌ Error in PDF upload promise:', error);
589 });
590 });
591
592 console.log('✅ WebViewer loaded successfully.');
593}).catch((error) => {
594 console.error('❌ Failed to initialize WebViewer:', error);
595});
596
1<html>
2 <head>
3 <script src='lib/webviewer.min.js'></script>
4 <title>Field Detection Demo</title>
5 </head>
6 <body style='width: 100%; height: 100%; padding: 0; margin: 0'>
7 <div id='viewer' style='width: 100%; height: 100%'></div>
8 <script src='index.js'></script>
9 </body>
10</html>
1/* Modal styles for field-detection demo */
2
3.modal-overlay {
4 position: fixed;
5 top: 0;
6 left: 0;
7 width: 100%;
8 height: 100%;
9 background-color: rgba(0, 0, 0, 0.5);
10 z-index: 1001;
11 display: flex;
12 justify-content: center;
13 align-items: center;
14}
15
16.modal-box {
17 background: white;
18 padding: 20px;
19 border-radius: 8px;
20 width: 80%;
21 max-width: 800px;
22 height: 80%;
23 max-height: 600px;
24 display: flex;
25 flex-direction: column;
26 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
27 position: relative;
28}
29
30.modal-controls {
31 margin-bottom: 15px;
32 display: flex;
33 gap: 10px;
34 align-items: center;
35}
36
37.modal-controls button {
38 padding: 8px 16px;
39 border: none;
40 border-radius: 4px;
41 cursor: pointer;
42 font-size: 14px;
43 transition: background-color 0.2s ease;
44}
45
46.modal-controls button:not(.modal-close) {
47 background: #007cba;
48 color: white;
49}
50
51.modal-controls button:not(.modal-close):hover {
52 background: #005a8b;
53}
54
55.modal-close {
56 background: #dc3545 !important;
57 color: white !important;
58 margin-left: auto;
59}
60
61.modal-close:hover {
62 background: #b02a37 !important;
63}
64
65.modal-content {
66 background: #f8f9fa;
67 padding: 15px;
68 border-radius: 4px;
69 overflow: auto;
70 flex: 1;
71 font-family: 'Courier New', monospace;
72 white-space: pre-wrap;
73 word-wrap: break-word;
74 border: 1px solid #dee2e6;
75 margin: 0;
76 color: #000000;
77}
78
1// ES6 Compliant Syntax
2// Copilot name: GitHub Copilot, version: 1.0, model: GPT-4, version: 2024-06, date: 2025-08-31
3// File: server/server.js
4// This file is to run a server in localhost.
5
6const express = require('express');
7const fs = require('fs');
8const bodyParser = require('body-parser');
9const open = (...args) => import('open').then(({ default: open }) => open(...args));
10const handler = require('./handler.js');
11const port = process.env.PORT || 5050;
12const app = express();
13const sentPdfs = 'sentPdfs';
14
15// CORS middleware to allow cross-origin requests from the playground
16app.use((req, res, next) => {
17 res.header('Access-Control-Allow-Origin', '*');
18 res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
19 res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
20
21 // Handle preflight OPTIONS requests
22 if (req.method === 'OPTIONS') {
23 res.sendStatus(200);
24 } else {
25 next();
26 }
27});
28
29app.use(bodyParser.text());
30app.use('/client', express.static('../client')); // For statically serving 'client' folder at '/'
31
32handler(app);
33
34// Run server
35const server = app.listen(port, 'localhost', (err) => {
36 if (err) {
37 console.error(err);
38 } else {
39 console.info(`Server is listening at http://localhost:${port}`);
40
41 }
42});
43
44// Server shutdown and cleanup
45function shutdown() {
46 console.log('Cleanup started...');
47
48 // Example: Close server
49 server.close(() => {
50 console.log('Server closed.');
51
52 // Removes sent PDFs folder
53 if (fs.existsSync(sentPdfs))
54 fs.rmdirSync(sentPdfs, { recursive: true });
55
56 // If no async cleanup, exit directly
57 process.exit(0);
58 });
59}
60
61// Handle shutdown signals
62process.on('SIGINT', shutdown); // Ctrl+C
63process.on('SIGTERM', shutdown); // kill command or Docker stop
64process.on('uncaughtException', (err) => {
65 console.error('Uncaught Exception:', err);
66 shutdown();
67});
1// ES6 Compliant Syntax
2// Copilot name: GitHub Copilot, version: 1.0, model: GPT-4, version: 2024-06, date: August 31, 2025
3// File: handler.js
4// This file will handle form fields detection requests by the server.
5
6const path = require('path');
7const fs = require('fs');
8const { PDFNet } = require('@pdftron/pdfnet-node');
9
10// **Important**
11// You must get a license key from Apryse for the server to run.
12// A trial key can be obtained from:
13// https://docs.apryse.com/core/guides/get-started/trial-key
14const licenseKey = 'demo:1723511765597:7e718f68030000000092e247505d40030c91eb451022bd89e74e6050f1';
15const multer = require('multer');
16const { response } = require('express');
17const upload = multer();
18const serverFolder = 'server';
19const sentPdfs = 'sentPdfs';
20const serverHandler = `/${serverFolder}/handler.js`;
21
22module.exports = async (app) => {
23
24 async function initializePDFNet() {
25 // Create folder sentPdfs that will hold the sent PDFs, if it doesn't exist
26 if (!fs.existsSync(sentPdfs))
27 fs.mkdirSync(sentPdfs);
28
29 // Initialize PDFNet
30 await PDFNet.initialize(licenseKey);
31
32 // Specify the data extraction library path
33 await PDFNet.addResourceSearchPath('./node_modules/@pdftron/data-extraction/lib/');
34
35 // Check if the Apryse SDK AIFormFieldExtractor module is available.
36 if (await PDFNet.DataExtractionModule.isModuleAvailable(PDFNet.DataExtractionModule.DataExtractionEngine.e_Form))
37 console.log('Apryse SDK AIFormFieldExtractor module is available.');
38 else
39 console.log('Unable to run Data Extraction: Apryse SDK AIFormFieldExtractor module not available.');
40 }
41
42 // Removes all sent PDFs
43 async function cleanupSentPdfs() {
44 if (fs.existsSync(sentPdfs)) {
45 await fs.promises.readdir(sentPdfs).then(async files => {
46 for (const file of files) {
47 const filePath = path.join(sentPdfs, file);
48 await fs.promises.unlink(filePath);
49 }
50 });
51 }
52 }
53
54 // Handle POST request sent to '/server/handler.js'
55 // This endpoint receives the currently loaded PDF file in the Apryse webviewer, then saves it to the server
56 app.post(serverHandler, upload.any(), async (request, response) => {
57 try {
58
59 // Removes previous sent PDFs
60 await cleanupSentPdfs();
61
62 const pdf = path.resolve(__dirname, `./${sentPdfs.split('/').pop()}/${request.query.filename}`);
63 fs.writeFileSync(pdf, request.files[0].buffer);
64 response.status(200).send(`Success saving PDF file ${request.query.filename}`);
65 } catch (e) {
66 response.status(500).send(`Error saving PDF file ${request.query.filename}`);
67 }
68 response.end();
69 });
70
71 // Handle GET request sent to '/server/handler.js'
72 // This endpoint extracts JSON data of form fields from the saved PDF file, then sends it back to the client
73 app.get(serverHandler, async (request, response) => {
74 let json = null;
75 response.header('Content-Type', 'application/json');
76 try {
77 const pdf = path.resolve(__dirname, `./${sentPdfs.split('/').pop()}/${request.query.filename}`);
78 if (fs.existsSync(pdf)) {
79
80 // Process the first page only.
81 const options = new PDFNet.DataExtractionModule.DataExtractionOptions();
82 options.setPages("1");
83
84 json = await PDFNet.DataExtractionModule.extractDataAsString(pdf, PDFNet.DataExtractionModule.DataExtractionEngine.e_Form, options);
85 }
86 response.status(200).send(json);
87 } catch (e) {
88 response.status(500).send(`Error extracting JSON data from PDF file ${request.query.filename}`);
89 }
90 response.end();
91 });
92
93 // Initialize PDFNet
94 PDFNet.runWithoutCleanup(initializePDFNet, licenseKey).then(
95 function onFulfilled() {
96 response.status(200);
97 },
98 function onRejected(error) {
99 // log error and close response
100 console.error('Error initializing PDFNet', error);
101 response.status(503).send();
102 }
103 );
104};
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales