> 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-document-structure-extraction.md).

# Extract Document Structure Showcase Demo Code Sample

Easily analyze the top two pages and produce a JSON that describes the PDF's structure.

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

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

<a href="/web/get-started/readme.md" class="button primary">Web SDK</a><a href="/core/get-started/get-started.md" class="button primary">Server 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/document-structure-extraction" class="button primary">Live demo</a>
{% endhint %}

Easily analyze the top two pages and produce a JSON that describes the PDF's structure. Preview the JSON data with a colorized legend for each extracted document element.

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 Smart Data Extraction Sample Code](/core/get-started/samples/dataextractiontest.md).

This demo allows you to:

* Upload your own PDF file
* Extract a JSON containing the elements in the PDF
* Create a colorized legend for each extracted element
* Update the PDF document to identify the extracted elements

### **Implementation steps**

To add document structure extraction capability with Server SDK and a UI 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

{% hint style="info" %}
In this demo, the first page in the JSON file is skipped as it contains a trial demo message. Processing starts on the next page after the trial demo page.
{% endhint %}

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 name: GitHub Copilot, version: 1.0.0, model: GPT-4, version: 2024-06, date: 2025-09-15
// 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/sales-invoice-with-credit-cards.pdf';
let jsonData = null;
const documentStructureMap = [
  { type: 'image', title: 'Image', color: { R: 1, G: 129, B: 1, A: 1 } },
  { type: 'paragraph', title: 'Paragraph', color: { R: 254, G: 254, B: 1, A: 1 } },
  { type: 'list', title: 'List', color: { R: 254, G: 129, B: 193, A: 1 } },
  { type: 'heading', title: 'Heading', color: { R: 254, G: 166, B: 1, A: 1 } },
  { type: 'header', title: 'Header', color: { R: 200, G: 20, B: 128, A: 1 } },
  { type: 'footer', title: 'Footer', color: { R: 3, G: 219, B: 252, A: 1 } },
  { type: 'graphic', title: 'Graphic', color: { R: 255, G: 0, B: 0, A: 1 } },
  { type: 'td', title: 'Table Data Cell', color: { R: 51, G: 101, B: 251, A: 1 } },
  { type: 'th', title: 'Table Header Cell', color: { R: 128, G: 0, B: 128, A: 1 } },
  { type: 'textbox', title: 'Text Box', color: { R: 20, G: 230, B: 50, A: 1 } },
  { type: 'group', title: 'Group', color: { R: 0, 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 document structure sub-panel to be registered
const documentStructurePanel = {
  handle: null,
  dataElement: 'documentStructurePanel',
  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 document structure sub-panel will be added to this Tab panel.
  tabPanel.handle = viewerPanels.find((panel) => panel.dataElement === tabPanel.dataElement);

  // Register the custom document structure sub-panel
  RegisterDocumentStructurePanel(instance);

  // Add the new custom document structure sub-panel to list of sub-panels under the Tab Panel
  documentStructurePanel.handle = { render: documentStructurePanel.dataElement };
  tabPanel.handle.panelsList = [documentStructurePanel.handle, ...tabPanel.handle.panelsList];

  UI.openElements([tabPanel.dataElement]);
  UI.setPanelWidth(tabPanel.dataElement, 400);
};

// Register the custom document structure sub-panel
const RegisterDocumentStructurePanel = (instance) => {
  documentStructurePanel.render = CreateDocumentStructurePanelElements(instance);
  instance.UI.addPanel({
    dataElement: documentStructurePanel.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: 'Document Structure',
    render: () => documentStructurePanel.render,
  });
};

// Create the document structure panel elements.
const CreateDocumentStructurePanelElements = (instance) => {
  let panelDiv = document.createElement('div');
  panelDiv.id = 'documentStructure';
  let paragraph = document.createTextNode('A sample PDF will have the first two pages analyzed and the resulting JSON can be viewed. After extraction, the displayed PDF will have annotations showing the identified elements.');
  panelDiv.appendChild(paragraph);

  const span = document.createElement("span");
  span.style.color = 'orange';
  span.appendChild(document.createTextNode('NOTE: Only the first two pages 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);

  // Extract document structure button
  let extractDocumentStructureButton = document.createElement('button');
  extractDocumentStructureButton.textContent = 'Extract Document Structure';
  extractDocumentStructureButton.id = 'extractDocumentStructureButton';
  extractDocumentStructureButton.onclick = async () => {

    extractDocumentStructureButton.style.cursor = "not-allowed"; // Changes cursor for the button itself
    documentStructurePanel.render.style.cursor = "not-allowed"; // Changes cursor for the button itself

    enableButton(extractDocumentStructureButton, false);
    await extractDocumentStructure(instance); // Extract document structure

    extractDocumentStructureButton.style.cursor = "default";
    documentStructurePanel.render.style.cursor = "default";
  }
  enableButton(extractDocumentStructureButton, false); // Initially disabled. Enabled after the PDF is sent to the server.

  panelDiv.appendChild(extractDocumentStructureButton);
  panelDiv.appendChild(document.createElement('p'));

  return panelDiv;
};

// Open JSON data in a viewer 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 a rectangle annotation for the given item on the specified page
const drawAnnotationRectangle = (instance, pageNumber, item) => {
  const { annotationManager, Annotations } = instance.Core;
  const color = documentStructureMap.find(field => field.type === item.type).color;
  const annot = new Annotations.RectangleAnnotation({
    PageNumber: pageNumber,
    X: item.rect[0],
    Y: item.rect[1],
    Width: item.rect[2] - item.rect[0],
    Height: item.rect[3] - item.rect[1],
    StrokeColor: new Annotations.Color(color.R, color.G, color.B, color.A),
    StrokeThickness: 2,
  });

  if (annot) {
    annotationManager.addAnnotation(annot);
    annotationManager.redrawAnnotation(annot);
  }
}

// Draw annotations on the PDF when extracting document structure
const drawAnnotations = (instance) => {

  jsonData.pages.forEach((page) => {
    page.elements.forEach((element) => {
      switch (element.type) {
        case 'table':
          element.trs.forEach((tr) => {
            tr.tds.forEach((td) => {
              drawAnnotationRectangle(instance, page.properties.pageNumber, td);
            });
          });
          break;
        case 'graphic':
          if (element.contents)
            element.contents.forEach((content) => {
              drawAnnotationRectangle(instance, page.properties.pageNumber, content);
            });
          else
            drawAnnotationRectangle(instance, page.properties.pageNumber, element);
          break;
        default:
          drawAnnotationRectangle(instance, page.properties.pageNumber, element);
          break
      }
    });
  });
}

// Remove trial mode page from JSON data.
// If demo license key is provided instead of production, 
// the first page of the received JSON data will contain
// a message indicating that Apryse SDK is running in trial mode.
const removeJSONTrialPage = (json) => {
  let pageIndexToRemove = -1;

  // Get the page index to remove
  for (const page of json.pages) {
    for (const element of page.elements) {
      if (element.contents &#x26;&#x26; element.contents.length) {
        for (const content of element.contents) {
          if (content.text &#x26;&#x26; content.text.includes('Apryse Data Extraction Module trial mode.')) {
            pageIndexToRemove = page.properties.pageNumber - 1;
            break;
          }
        }
      }

      if (pageIndexToRemove !== -1)
        break;
    }
    if (pageIndexToRemove !== -1)
      break;
  }

  // Remove the page and update page numbers
  if (pageIndexToRemove !== -1) {
    json.pages.splice(pageIndexToRemove, 1);
    for (let i = 0; i &#x3C; json.pages.length; i++) {
      json.pages[i].properties.pageNumber = i + 1;
    }
  }

  return json;
};

// Extract document structure from the PDF document
// This function will send GET message to the server,
// to receive the extracted document structure as JSON object.
const extractDocumentStructure = async (instance) => {

  const doc = instance.Core.documentViewer.getDocument();

  // Make a GET request to get the extracted JSON data of document structure 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.parse(json);

          // check if the received JSON data is valid
          if (jsonData === null || jsonData === 'undefined' || !jsonData.pages) {
            console.error('❌ Received invalid JSON data from server');
            resolve();
            return;
          }

          jsonData = removeJSONTrialPage(jsonData); // remove trial mode page from JSON data
          let jsonText = JSON.stringify(jsonData, null, 2);
          jsonText = jsonText.replace(/\\r\\n/g, '\n');
          jsonText = jsonText.replace(/\\"/g, '"');

          // Display the extracted document structure color legend
          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
          documentStructureMap.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);
          });

          documentStructurePanel.render.appendChild(colorsDiv);

          // Display the extracted document structure JSON data
          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'));

          documentStructurePanel.render.appendChild(jsonDiv);
          drawAnnotations(instance);
          resolve();
        })
      }
      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 document structure JSON data when the user clicks the "Extract Document Structure" 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 Extract Document Structure button
          const extractButton = documentStructurePanel.render.querySelector('#extractDocumentStructureButton');
          if (extractButton) {
            console.log('🔓 Enabling Extract Document Structure button');
            enableButton(extractButton, true);
          } else {
            console.warn('⚠️ Could not find extractDocumentStructureButton 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 document structure extraction 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>Document Structure Extraction 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 document-structure-extraction 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.0, model: GPT-4, version: 2024-06, date: 2025-09-15
// File: server/server.js

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.0, model: GPT-4, version: 2024-06, date: 2025-09-15
// File: server/handler.js

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 Structured Output module is available.
    if (await PDFNet.DataExtractionModule.isModuleAvailable(PDFNet.DataExtractionModule.DataExtractionEngine.e_DocStructure))
      console.log('Apryse SDK Structured Output module is available.');
    else
      console.log('Unable to run Data Extraction: Apryse SDK Structured Output 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 two pages only.
        const options = new PDFNet.DataExtractionModule.DataExtractionOptions();
        options.setPages("1-2"); // Extract from page 1 to 2

        json = await PDFNet.DataExtractionModule.extractDataAsString(pdf, PDFNet.DataExtractionModule.DataExtractionEngine.e_DocStructure, 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 %}


---

# 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-document-structure-extraction.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.
