Effortlessly detect and classify Form Fields. Transform your flat PDF into an interactive form with editable fields using AI-powered recognition.
This sample code includes Server SDK processing in JavaScript, with UI provided by WebViewer. If a viewer is not needed, or you want to work with a different language or framework for the Server SDK, please check out our Server SDK Smart Data Extraction Sample Code.
This demo allows you to:
Implementation steps
To add PDF form field detection capability with WebViewer:
Step 1: Follow get-started in JavaScript for Server SDK
Step 2: Follow get-started in your preferred web stack for WebViewer
Step 3: Download Data Extraction Module
Step 4: Add the ES6 JavaScript sample code provided in this guide
Once you generate your license key, it will automatically be included in your sample code below.
Apryse collects some data regarding your usage of the SDK for product improvement.
The data that Apryse collects include:
For clarity, no other data is collected by the SDK and Apryse has no access to the contents of your documents.
If you wish to continue without data collection, contact us and we will email you a no-tracking trial key for you to get started.
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