// 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 = 'YOUR_WEBVIEWER_LICENSE_KEY';
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: '<svg width="18px" height="18px" viewBox="0 0 24 24" id="圖層_1" data-name="圖層 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#080808;}</style></defs><title>form</title><path class="cls-1" d="M21,.5H3a2,2,0,0,0-2,2V22a2,2,0,0,0,2,2H21a2,2,0,0,0,2-2V2.5A2,2,0,0,0,21,.5Zm0,2v2H3v-2ZM3,22V6.5H21V22Z"/><path class="cls-1" d="M12.5,4H20a.5.5,0,0,0,0-1H12.5a.5.5,0,0,0,0,1Z"/><path class="cls-1" d="M4.5,4a.43.43,0,0,0,.19,0,.35.35,0,0,0,.16-.11A.47.47,0,0,0,5,3.5a.43.43,0,0,0,0-.19.36.36,0,0,0-.11-.16.5.5,0,0,0-.7,0A.35.35,0,0,0,4,3.31.43.43,0,0,0,4,3.5a.51.51,0,0,0,.5.5Z"/><path class="cls-1" d="M5.65,3.85A.36.36,0,0,0,5.81,4,.44.44,0,0,0,6,4a.47.47,0,0,0,.35-.15.36.36,0,0,0,.11-.16.6.6,0,0,0,0-.19.51.51,0,0,0-.15-.35A.49.49,0,0,0,5.81,3a.36.36,0,0,0-.16.11.47.47,0,0,0-.15.35.4.4,0,0,0,0,.19A.35.35,0,0,0,5.65,3.85Z"/><path class="cls-1" d="M8,8H4.5a1,1,0,0,0,0,2H8A1,1,0,0,0,8,8Z"/><path class="cls-1" d="M8,11.67H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M8,15.33H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M8,19H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,8H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,11.67H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,15.33H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,19H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,8h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,11.67h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,15.33h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,19h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/></svg>',
title: '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 && element.contents.length) {
for (const content of element.contents) {
if (content.text && 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 < 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);
});