> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/web/get-started/samples/showcase-demo-field-detection.md).

# Field Detection Showcase Demo Code Sample

Form field detection and classification in a flat PDF using AI

{% hint style="info" %}
**Requirements**

*These packages are required to use these features in production. Trial keys have unlimited access to all features*

<a href="/core/get-started/get-started.md" class="button primary">Server SDK</a><a href="/web/get-started/readme.md" class="button primary">Web SDK</a><a href="https://apryse.com/capabilities#SmartDataExtraction" class="button primary">Package: Smart Data Extraction</a><a href="/core/learn-more/modules.md#data-extraction-module" class="button primary">Module: Data Extraction</a><a href="https://showcase.apryse.com/field-detection" class="button primary">Live demo</a>
{% endhint %}

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](/core/get-started/samples/dataextractiontest.md).

This demo allows you to:

* Upload a flat PDF and automatically detect form elements such as:
  * Text Fields
  * Check Boxes
  * Radio Buttons
  * List Boxes
  * Combo Boxes
  * Buttons
  * Digital Signatures
* Convert detected elements into editable form fields.
* Edit and customize the newly created fields, then save your updated document.

**Implementation steps** To add PDF form field detection capability with WebViewer:

Step 1: Follow get-started in [JavaScript for Server SDK](/core/get-started/frameworks/nodejs.md) Step 2: Follow get-started in your [preferred web stack for WebViewer](/web/get-started/readme.md) Step 3: [Download Data Extraction Module](/core/learn-more/modules.md#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-license-key/apryse-license-key platform="WEB\_VIEWER" variant="compact" %}

{% tabs %}
{% tab title="index.js" %}

<pre class="language-js" data-line-numbers><code class="lang-js">// 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 = '<code class="expression">visitor.claims.wvKey || "YOUR_WEBVIEWER_LICENSE_KEY"</code>';
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: '&#x3C;svg width="18px" height="18px" viewBox="0 0 24 24" id="圖層_1" data-name="圖層 1" xmlns="http://www.w3.org/2000/svg">&#x3C;defs>&#x3C;style>.cls-1{fill:#080808;}&#x3C;/style>&#x3C;/defs>&#x3C;title>form&#x3C;/title>&#x3C;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"/>&#x3C;path class="cls-1" d="M12.5,4H20a.5.5,0,0,0,0-1H12.5a.5.5,0,0,0,0,1Z"/>&#x3C;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"/>&#x3C;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"/>&#x3C;path class="cls-1" d="M8,8H4.5a1,1,0,0,0,0,2H8A1,1,0,0,0,8,8Z"/>&#x3C;path class="cls-1" d="M8,11.67H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M8,15.33H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M8,19H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M14,8H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M14,11.67H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M14,15.33H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M14,19H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M19.5,8h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M19.5,11.67h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M19.5,15.33h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M19.5,19h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/>&#x3C;/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);
});

</code></pre>

{% endtab %}

{% tab title="index.html" %}
{% code title="index.html" lineNumbers="true" %}

```html
<html>
  <head>
    <script src='lib/webviewer.min.js'></script>
    <title>Field Detection Demo</title>
  </head>
  <body style='width: 100%; height: 100%; padding: 0; margin: 0'>
    <div id='viewer' style='width: 100%; height: 100%'></div>
    <script src='index.js'></script>
  </body>
</html>
```

{% endcode %}
{% endtab %}

{% tab title="modal.css" %}
{% code title="modal.css" lineNumbers="true" %}

```css
/* Modal styles for field-detection demo */

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 1001;
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-box {
  background: white;
  padding: 20px;
  border-radius: 8px;
  width: 80%;
  max-width: 800px;
  height: 80%;
  max-height: 600px;
  display: flex;
  flex-direction: column;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  position: relative;
}

.modal-controls {
  margin-bottom: 15px;
  display: flex;
  gap: 10px;
  align-items: center;
}

.modal-controls button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.2s ease;
}

.modal-controls button:not(.modal-close) {
  background: #007cba;
  color: white;
}

.modal-controls button:not(.modal-close):hover {
  background: #005a8b;
}

.modal-close {
  background: #dc3545 !important;
  color: white !important;
  margin-left: auto;
}

.modal-close:hover {
  background: #b02a37 !important;
}

.modal-content {
  background: #f8f9fa;
  padding: 15px;
  border-radius: 4px;
  overflow: auto;
  flex: 1;
  font-family: 'Courier New', monospace;
  white-space: pre-wrap;
  word-wrap: break-word;
  border: 1px solid #dee2e6;
  margin: 0;
  color: #000000;
}

```

{% endcode %}
{% endtab %}

{% tab title="server.js" %}
{% code title="server.js" lineNumbers="true" %}

```js
// ES6 Compliant Syntax
// Copilot name: GitHub Copilot, version: 1.0, model: GPT-4, version: 2024-06, date: 2025-08-31
// File: server/server.js
// This file is to run a server in localhost.

const express = require('express');
const fs = require('fs');
const bodyParser = require('body-parser');
const open = (...args) => import('open').then(({ default: open }) => open(...args));
const handler = require('./handler.js');
const port = process.env.PORT || 5050;
const app = express();
const sentPdfs = 'sentPdfs';

// CORS middleware to allow cross-origin requests from the playground
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
  
  // Handle preflight OPTIONS requests
  if (req.method === 'OPTIONS') {
    res.sendStatus(200);
  } else {
    next();
  }
});

app.use(bodyParser.text());
app.use('/client', express.static('../client')); // For statically serving 'client' folder at '/'

handler(app);

// Run server
const server = app.listen(port, 'localhost', (err) => {
  if (err) {
    console.error(err);
  } else {
    console.info(`Server is listening at http://localhost:${port}`);
    
  }
});

// Server shutdown and cleanup
function shutdown() {
  console.log('Cleanup started...');

  // Example: Close server
  server.close(() => {
    console.log('Server closed.');

    // Removes sent PDFs folder
    if (fs.existsSync(sentPdfs))
      fs.rmdirSync(sentPdfs, { recursive: true });

    // If no async cleanup, exit directly
    process.exit(0);
  });
}

// Handle shutdown signals
process.on('SIGINT', shutdown);   // Ctrl+C
process.on('SIGTERM', shutdown);  // kill command or Docker stop
process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  shutdown();
});
```

{% endcode %}
{% endtab %}

{% tab title="handler.js" %}

<pre class="language-js" data-line-numbers><code class="lang-js">// ES6 Compliant Syntax
// Copilot name: GitHub Copilot, version: 1.0, model: GPT-4, version: 2024-06, date: August 31, 2025
// File: handler.js
// This file will handle form fields detection requests by the server.

const path = require('path');
const fs = require('fs');
const { PDFNet } = require('@pdftron/pdfnet-node');

// **Important** 
// You must get a license key from Apryse for the server to run. 
// A trial key can be obtained from:
// https://docs.apryse.com/core/guides/get-started/trial-key 
const licenseKey =  '<code class="expression">visitor.claims.wvKey || "YOUR_SERVER_LICENSE_KEY"</code>'; 
const multer = require('multer');
const { response } = require('express');
const upload = multer();
const serverFolder = 'server';
const sentPdfs = 'sentPdfs';
const serverHandler = `/${serverFolder}/handler.js`;

module.exports = async (app) => {

  async function initializePDFNet() {
    // Create folder sentPdfs that will hold the sent PDFs, if it doesn't exist
    if (!fs.existsSync(sentPdfs))
      fs.mkdirSync(sentPdfs);

    // Initialize PDFNet
    await PDFNet.initialize(licenseKey);

    // Specify the data extraction library path
    await PDFNet.addResourceSearchPath('./node_modules/@pdftron/data-extraction/lib/');

    // Check if the Apryse SDK AIFormFieldExtractor module is available.
    if (await PDFNet.DataExtractionModule.isModuleAvailable(PDFNet.DataExtractionModule.DataExtractionEngine.e_Form))
      console.log('Apryse SDK AIFormFieldExtractor module is available.');
    else
      console.log('Unable to run Data Extraction: Apryse SDK AIFormFieldExtractor module not available.');
  }

  // Removes all sent PDFs
  async function cleanupSentPdfs() {
    if (fs.existsSync(sentPdfs)) {
      await fs.promises.readdir(sentPdfs).then(async files => {
        for (const file of files) {
          const filePath = path.join(sentPdfs, file);
          await fs.promises.unlink(filePath);
        }
      });
    }
  }

  // Handle POST request sent to '/server/handler.js'
  // This endpoint receives the currently loaded PDF file in the Apryse webviewer, then saves it to the server
  app.post(serverHandler, upload.any(), async (request, response) => {
    try {

      // Removes previous sent PDFs
      await cleanupSentPdfs();

      const pdf = path.resolve(__dirname, `./${sentPdfs.split('/').pop()}/${request.query.filename}`);
      fs.writeFileSync(pdf, request.files[0].buffer);
      response.status(200).send(`Success saving PDF file ${request.query.filename}`);
    } catch (e) {
      response.status(500).send(`Error saving PDF file ${request.query.filename}`);
    }
    response.end();
  });

  // Handle GET request sent to '/server/handler.js'
  // This endpoint extracts JSON data of form fields from the saved PDF file, then sends it back to the client
  app.get(serverHandler, async (request, response) => {
    let json = null;
    response.header('Content-Type', 'application/json');
    try {
      const pdf = path.resolve(__dirname, `./${sentPdfs.split('/').pop()}/${request.query.filename}`);
      if (fs.existsSync(pdf)) {

        // Process the first page only.
        const options = new PDFNet.DataExtractionModule.DataExtractionOptions();
        options.setPages("1");

        json = await PDFNet.DataExtractionModule.extractDataAsString(pdf, PDFNet.DataExtractionModule.DataExtractionEngine.e_Form, options);
      }
      response.status(200).send(json);
    } catch (e) {
      response.status(500).send(`Error extracting JSON data from PDF file ${request.query.filename}`);
    }
    response.end();
  });

  // Initialize PDFNet
  PDFNet.runWithoutCleanup(initializePDFNet, licenseKey).then(
    function onFulfilled() {
      response.status(200);
    },
    function onRejected(error) {
      // log error and close response
      console.error('Error initializing PDFNet', error);
      response.status(503).send();
    }
  );
};
</code></pre>

{% endtab %}
{% endtabs %}

[View the full sample on GitHub](https://github.com/ApryseSDK/webviewer-samples/tree/main/showcase-demos-playground)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.apryse.com/web/get-started/samples/showcase-demo-field-detection.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
