Quickly extract key-value pairs from PDFs, convert values into JSON for easy analysis, and display annotations that highlight each paired element.
This demo allows you to:
To add key-value extraction capability with WebViewer:
Step 1: Choose your preferred web stack for WebViewer
Step 2: 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// GitHub Copilot v1.0, Claude Sonnet 4, November 13, 2025
3// File: index.js
4
5import WebViewer from '@pdftron/webviewer';
6
7// Key-Value Extraction Demo
8// This code demonstrates how to extract key-value pairs from documents using WebViewer
9//
10// **Important**
11// 1. 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
14//
15// 2. You need to also run the `npm install` command at /key-value-extraction/server/ location to install the `@pdftron/pdfnet-node` and `@pdftron/data-extraction` packages.
16
17function initializeWebViewer() {
18
19 // This code initializes the WebViewer with the basic settings
20 WebViewer({
21 path: '/lib',
22 licenseKey: 'YOUR_LICENSE_KEY',
23 enableFilePicker: true,
24 }, document.getElementById('viewer')).then((instance) => {
25
26 // Add the demo-specific functionality
27 customizeUI(instance).then(() => {
28 // Create UI controls after demo is initialized
29 UIElements.createUIControls(instance);
30 });
31 });
32}
33
34// Starting page for extraction
35let startPage = 1;
36
37// Global variable to hold result data
38window.resultData = null;
39
40// Custom File class to hold file metadata (not to be confused with browser's File API)
41class FileMetadata {
42 constructor(options) {
43 this.name = options.name;
44 this.displayName = options.displayName;
45 this.path = options.path;
46 this.extension = options.extension;
47 this.displayExtension = options.displayExtension;
48 this.id = options.id;
49 }
50}
51
52const files = {
53 DRIVERS_LICENSE: new FileMetadata({
54 name: 'sample-license.pdf',
55 displayName: 'Driver\'s License',
56 path: 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/sample-license.pdf',
57 extension: 'pdf',
58 id: 100
59 }),
60 SALES_INVOICE: new FileMetadata({
61 name: 'sales-invoice.pdf',
62 displayName: 'Sales Invoice',
63 path: 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/sales-invoice.pdf',
64 extension: 'pdf',
65 id: 14
66 })
67}
68
69const sampleDocuments = [
70 files.DRIVERS_LICENSE,
71 files.SALES_INVOICE
72];
73window.sampleDocuments = sampleDocuments;
74
75const defaultFile = sampleDocuments[1].path; // SALES_INVOICE
76
77const customizeUI = async (instance) => {
78 // Customize the UI for the key value extraction demo
79 instance.UI.setToolbarGroup('toolbarGroup-View');
80 instance.UI.disableElements(['thumbnailControl']);
81
82 // Reset variables when new document is loaded
83 instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
84 window.resultData = null;
85 startPage = 1;
86
87 // Reset the JSON display area and Color Legend
88 UIElements.resetUI(instance);
89 });
90
91 // Load the default file for demonstration
92 if (defaultFile) {
93 instance.UI.loadDocument(defaultFile);
94 }
95};
96
97// Function to extract key-value pairs from title block via server
98const extractKeyValuePairs = async (instance) => {
99 const doc = instance.Core.documentViewer.getDocument();
100 if (doc) {
101 const pdfBuffer = await doc.getFileData({ flags: instance.Core.SaveOptions.LINEARIZED });
102 console.log('Sending PDF to server for key-value extraction...');
103 const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' });
104 const formData = new FormData();
105 formData.append('pdffile', pdfBlob, 'viewerDocument.pdf');
106
107 // Send the PDF to the server to extract key-value pairs
108 const postResponse = await fetch('http://localhost:5050/server/handler.js/extract-key-value-pairs', {
109 method: 'POST',
110 body: formData,
111 });
112
113 if (postResponse.status !== 200) {
114 throw new Error(`Server error during PDF upload: ${postResponse.status}`);
115 }
116
117 // Retrieve and parse the JSON response
118 const jsonResponse = await postResponse.json();
119 const docStructureData = JSON.parse(jsonResponse);
120 resultData = JSON.stringify(docStructureData, null, 2);
121
122 // Draw annotations on the document based on extracted data
123 drawAnnotations(docStructureData, instance);
124 return;
125 }
126}
127
128window.extractKeyValuePairs = extractKeyValuePairs; // Make extractKeyValuePairs globally available so that the UIElements module can access it
129
130// Function to draw annotations on the document based on extracted key-value data
131const drawAnnotations = (docStructureData, instance) => {
132 const { annotationManager, Annotations } = instance.Core;
133
134 // Retrieve the first page's data
135 const page = docStructureData.pages[startPage - 1];
136 const pageNumber = page?.properties?.pageNumber;
137 console.log(`Processing Page ${pageNumber} for annotations...`);
138 for (const kv of page.keyValueElements ?? []) {
139 const valueRect = kv?.rect;
140 const keyRect = kv?.key?.rect;
141 const hasValueWords = (kv?.words?.length ?? 0) > 0;
142
143 // Only draw if value has words
144 if (!hasValueWords) {
145 console.log('Skipping annotation for key-value pair with no value words.');
146 continue;
147 }
148 // value: blue
149 const valueAnnot = new Annotations.RectangleAnnotation({
150 PageNumber: pageNumber,
151 X: valueRect[0],
152 Y: valueRect[1],
153 Width: valueRect[2] - valueRect[0],
154 Height: valueRect[3] - valueRect[1],
155 StrokeColor: new Annotations.Color(0, 0, 255),
156 StrokeThickness: 1,
157 });
158 annotationManager.addAnnotation(valueAnnot);
159 annotationManager.redrawAnnotation(valueAnnot);
160
161 // key: red
162 const keyAnnot = new Annotations.RectangleAnnotation({
163 PageNumber: pageNumber,
164 X: keyRect[0],
165 Y: keyRect[1],
166 Width: keyRect[2] - keyRect[0],
167 Height: keyRect[3] - keyRect[1],
168 StrokeColor: new Annotations.Color(255, 0, 0),
169 StrokeThickness: 1,
170 });
171 annotationManager.addAnnotation(keyAnnot);
172 annotationManager.redrawAnnotation(keyAnnot);
173
174 // Green connector
175 const line = new Annotations.LineAnnotation();
176 line.pageNumber = pageNumber;
177 line.StrokeColor = new Annotations.Color(0, 255, 0);
178 line.StrokeThickness = 1;
179 line.Start = topLeftPoint(valueRect, instance);
180 line.End = topLeftPoint(keyRect, instance);
181 annotationManager.addAnnotation(line);
182 annotationManager.redrawAnnotation(line);
183 }
184};
185
186// Helper function to get top-left point of a rectangle
187const topLeftPoint = ([x1, y1, x2, y2], instance) => {
188 return new instance.Core.Math.Point(Math.min(x1, x2), Math.min(y1, y2));
189};
190
191// Cleanup function for when the demo is closed or page is unloaded
192const cleanup = (instance) => {
193 if (typeof instance !== 'undefined' && instance.UI) {
194 if (instance.Core.documentViewer.getDocument()) {
195 // Insert any other cleanup code here
196 }
197 console.log('Cleaning up title-block-data-extraction demo');
198 }
199};
200
201// Register cleanup for page unload
202window.addEventListener('beforeunload', () => cleanup(instance));
203window.addEventListener('unload', () => cleanup(instance));
204
205// Helper function to load the ui-elements.js script
206function loadUIElementsScript() {
207 return new Promise((resolve, reject) => {
208 if (window.UIElements) {
209 console.log('UIElements already loaded');
210 resolve();
211 return;
212 }
213 const script = document.createElement('script');
214 script.src = '/showcase-demos/key-value-extraction/client/ui-elements.js';
215 script.onload = function () {
216 console.log('✅ UIElements script loaded successfully');
217 resolve();
218 };
219 script.onerror = function () {
220 console.error('Failed to load UIElements script');
221 reject(new Error('Failed to load ui-elements.js'));
222 };
223 document.head.appendChild(script);
224 });
225}
226
227// Load UIElements script first, then initialize WebViewer
228loadUIElementsScript().then(() => {
229 initializeWebViewer();
230}).catch((error) => {
231 console.error('Failed to load UIElements:', error);
232});
233
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, Claude Sonnet 4, November 13, 2025
3// File: handler.js
4// This file will handle key-value extraction requests.
5
6const fs = require('fs');
7const { PDFNet } = require('@pdftron/pdfnet-node');
8
9// **Important**
10// 1. You must get a license key from Apryse for the server to run.
11// A trial key can be obtained from:
12// https://docs.apryse.com/core/guides/get-started/trial-key
13//
14// 2. You need to also run the `npm install` command at /key-value-extraction/server/ location to install the `@pdftron/pdfnet-node` and `@pdftron/data-extraction` packages.
15const licenseKey = 'YOUR_LICENSE_KEY';
16const multer = require('multer');
17const storage = multer.diskStorage({
18 destination: function (req, file, cb) {
19 cb(null, 'sentFiles/')
20 },
21 filename: function (req, file, cb) {
22 // Save with original filename and extension
23 cb(null, file.originalname)
24 }
25});
26const upload = multer({ storage: storage });
27const { response } = require('express');
28const e = require('express');
29const serverFolder = 'server';
30const sentFiles = 'sentFiles';
31const serverHandler = `/${serverFolder}/handler.js`;
32
33module.exports = async (app) => {
34
35 // Function to initialize PDFNet and check for module availability
36 async function initializePDFNet() {
37 // Create folder sentFiles that will hold the sent files, if it doesn't exist
38 if (!fs.existsSync(sentFiles))
39 fs.mkdirSync(sentFiles);
40
41 // Initialize PDFNet
42 await PDFNet.initialize(licenseKey);
43
44 // Specify the Data Extraction library path
45 await PDFNet.addResourceSearchPath('./node_modules/@pdftron/data-extraction/lib/');
46
47 // Check if the Apryse SDK Data Extraction module is available.
48 if (await PDFNet.DataExtractionModule.isModuleAvailable(PDFNet.DataExtractionModule.DataExtractionEngine.e_GenericKeyValue))
49 console.log('Apryse SDK Data Extraction module is available.');
50 else
51 console.log('Unable to run: Apryse SDK Data Extraction module not available.');
52 }
53
54 // Handle POST request sent to '/server/handler.js/extract-key-value-pairs'
55 // This endpoint receives the PDF file path, extracts key-value data from the title block, and returns it as JSON
56 app.post(`${serverHandler}/extract-key-value-pairs`, upload.single('pdffile'), async (request, response) => {
57 try {
58 console.log('Received PDF for key-value extraction');
59 const pdfPath = request.file.path;
60 const jsonResponse = await extractKeyValuePairs(pdfPath, 'title_block_template.json');
61 response.status(200).json(jsonResponse);
62 } catch (error) {
63 console.error('Error extracting key-value data:', error);
64 response.status(500).send('Error extracting key-value data');
65 } finally {
66 // Cleanup: remove the sent PDF file
67 const pdfPath = request.file.path;
68 fs.unlink(pdfPath, (err) => {
69 if (err) {
70 console.error(`Error removing PDF file ${pdfPath}: ${err.message}`);
71 }
72 });
73 }
74 });
75
76 // Function to extract key-value pairs from PDF using Data Extraction module
77 const extractKeyValuePairs = async (pdf) => {
78 try {
79 // Set up data extraction options
80 const options = new PDFNet.DataExtractionModule.DataExtractionOptions();
81 console.log('Setting extraction language to English');
82 options.setLanguage('eng');
83 options.setPages('1-1'); // Extract from first page only
84
85 // Extract key-value data from the PDF using the provided JSON template
86 const jsonString = await PDFNet.DataExtractionModule.extractDataAsString(pdf, PDFNet.DataExtractionModule.DataExtractionEngine.e_GenericKeyValue, options);
87 return jsonString;
88
89 } catch (err) {
90 console.log(err);
91 throw new Error(err);
92 }
93 };
94
95 // Initialize PDFNet
96 PDFNet.runWithoutCleanup(initializePDFNet, licenseKey).then(
97 function onFulfilled() {
98 response.status(200);
99 },
100 function onRejected(error) {
101 // log error and close response
102 console.error('Error initializing PDFNet', error);
103 response.status(503).send();
104 }
105 );
106};
107
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, Claude Sonnet 4, November 13, 2025
3// File: 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 handler = require('./handler.js');
10const port = process.env.PORT || 5050;
11const app = express();
12const sentPdfs = 'sentPdfs';
13
14// CORS middleware to allow cross-origin requests from the playground
15app.use((req, res, next) => {
16 res.header('Access-Control-Allow-Origin', '*');
17 res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
18 res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
19
20 // Handle preflight OPTIONS requests
21 if (req.method === 'OPTIONS') {
22 res.sendStatus(200);
23 } else {
24 next();
25 }
26});
27
28app.use(bodyParser.text());
29app.use('/client', express.static('../client')); // For statically serving 'client' folder at '/'
30
31handler(app);
32
33// Run server
34const server = app.listen(port, 'localhost', (err) => {
35 if (err) {
36 console.error(err);
37 } else {
38 console.info(`Server is listening at http://localhost:${port}`);
39 }
40});
41
42// Server shutdown and cleanup
43function shutdown() {
44 console.log('Cleanup started...');
45
46 // Example: Close server
47 server.close(() => {
48 console.log('Server closed.');
49
50 // Removes sent PDFs folder
51 if (fs.existsSync(sentPdfs))
52 fs.rmdirSync(sentPdfs, { recursive: true });
53
54 // If no async cleanup, exit directly
55 process.exit(0);
56 });
57}
58
59// Handle shutdown signals
60process.on('SIGINT', shutdown); // Ctrl+C
61process.on('SIGTERM', shutdown); // kill command or Docker stop
62process.on('uncaughtException', (err) => {
63 console.error('Uncaught Exception:', err);
64 shutdown();
65});
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, Claude Sonnet 4, November 13, 2025
3// File: ui-elements.js
4// UI Elements class to create and manage custom UI controls
5// Helper code to add controls to the viewer holding the buttons
6
7class UIElements {
8 // Function to check if the user is on a mobile device
9 static isMobileDevice = () => {
10 return (
11 /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(
12 window.navigator.userAgent
13 ) ||
14 /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
15 window.navigator.userAgent.substring(0, 4)
16 )
17 );
18 }
19
20 // JSON Code Block Element
21 static jsonElement = () => {
22 const wrapper = document.createElement('div');
23 wrapper.className = 'json-wrapper';
24 wrapper.style.display = 'none'; // Initially hidden
25
26 // Container for the JSON code block
27 const container = document.createElement('div');
28 container.className = 'json-container';
29
30 // Code block for JSON
31 const codePre = document.createElement('pre');
32 codePre.className = 'json-pre';
33
34 const codeBlock = document.createElement('code');
35 codeBlock.id = 'json-code';
36 codeBlock.contentEditable = false;
37
38 // Assemble the JSON code block
39 codePre.appendChild(codeBlock);
40 container.appendChild(codePre);
41 wrapper.appendChild(container);
42 return wrapper;
43 };
44
45 // Extract Key Value Pairs button
46 static extractKeyValuePairsButton = (instance) => {
47 // Spinner element to indicate loading
48 const spinner = document.createElement('div');
49 spinner.className = 'spinner';
50
51 // Button element for extracting key-value pairs
52 const button = document.createElement('button');
53 button.className = 'btn extract-btn';
54 button.textContent = 'Extract Key-Value Pairs';
55 button.onclick = async () => {
56 try {
57 button.disabled = true;
58 spinner.style.display = 'inline-block';
59 await window.extractKeyValuePairs(instance);
60 } catch (e) {
61 console.error(e);
62 button.disabled = false;
63 }
64 finally {
65 // Hide spinner when done
66 spinner.style.display = 'none';
67
68 // Display the extracted JSON data
69 const jsonWrapper = document.querySelector('.json-wrapper');
70 const jsonCodeBlock = document.getElementById('json-code');
71 if (window.resultData) {
72 jsonCodeBlock.textContent = JSON.stringify(JSON.parse(window.resultData), null, 2);
73 jsonWrapper.style.display = 'flex';
74 } else {
75 jsonCodeBlock.textContent = '';
76 jsonWrapper.style.display = 'none';
77 }
78
79 // Show color legend if extraction was successful
80 const legendContainer = document.querySelector('.legend-container');
81 if (window.resultData) {
82 legendContainer.style.display = 'flex';
83 } else {
84 legendContainer.style.display = 'none';
85 }
86 }
87 };
88
89 button.appendChild(spinner);
90
91 return button;
92 }
93
94 // Legends for the Annotations
95 static colorLegend = () => {
96 const legendContainer = document.createElement('div');
97 legendContainer.className = 'legend-container';
98
99 const colors = ['rgb(255, 0, 0)', 'rgb(0, 0, 255)', 'rgb(0, 255, 0)'];
100 const labels = ['Key', 'Value', 'Connector'];
101
102 for (let i = 0; i < colors.length; i++) {
103 const legendItem = document.createElement('div');
104 legendItem.className = 'legend-item';
105
106 const colorBox = document.createElement('span');
107 colorBox.className = 'color-box';
108 colorBox.style.backgroundColor = colors[i];
109
110 const label = document.createElement('span');
111 label.textContent = labels[i];
112
113 legendItem.appendChild(colorBox);
114 legendItem.appendChild(label);
115 legendContainer.appendChild(legendItem);
116 }
117
118 return legendContainer;
119 }
120
121 // Reset JSON Code Block and Legend
122 static resetUI = () => {
123 // Hide JSON code block
124 const jsonWrapper = document.querySelector('.json-wrapper');
125 const jsonCodeBlock = document.getElementById('json-code');
126 jsonCodeBlock.textContent = '';
127 jsonWrapper.style.display = 'none';
128
129 // Hide legend
130 const legendContainer = document.querySelector('.legend-container');
131 legendContainer.style.display = 'none';
132
133 // Enable Extract button
134 const extractButton = document.querySelector('.extract-btn');
135 extractButton.disabled = false;
136 }
137
138
139 // Gallery Picker Element
140 static galleryPicker = (instance) => {
141 const galleryGrid = document.createElement('div');
142 galleryGrid.className = 'gallery-grid';
143
144 const sampleDocuments = window.sampleDocuments;
145 const filesArray = [
146 { name: sampleDocuments[0].displayName, thumbnail: '/showcase-demos/key-value-extraction/client/gallery/thumbs/sample-license.png', url: sampleDocuments[0].path },
147 { name: sampleDocuments[1].displayName, thumbnail: '/showcase-demos/key-value-extraction/client/gallery/thumbs/sales-invoice.png' , url: sampleDocuments[1].path }
148 ];
149
150 // Render the grid of thumbnails
151 for (const file of filesArray) {
152 const thumbDiv = document.createElement('div');
153 thumbDiv.className = 'gallery-thumb';
154 thumbDiv.title = file.name;
155 thumbDiv.tabIndex = 0;
156 thumbDiv.setAttribute('role', 'button');
157 thumbDiv.setAttribute('aria-label', file.name);
158 thumbDiv.onclick = () => handleClick(file);
159 thumbDiv.onkeydown = (e) => {
160 if (e.key === 'Enter' || e.key === ' ') {
161 handleClick(file);
162 }
163 };
164
165 const img = document.createElement('img');
166 img.src = file.thumbnail;
167 img.alt = file.displayName;
168 img.className = 'gallery-thumb-img';
169 thumbDiv.appendChild(img);
170
171 const label = document.createElement('div');
172 label.className = 'gallery-thumb-label';
173 label.textContent = `${file.name}`;
174 label.onclick = (e) => {
175 e.stopPropagation();
176 fetch(file.url).then(response => {
177 if (!response.ok) {
178 throw new Error('Could not fetch file: ' + response.statusText);
179 }
180 return response.blob();
181 }).then(blob => {
182 const url = window.URL.createObjectURL(blob);
183 const a = document.createElement('a');
184 a.style.display = 'none';
185 a.href = url;
186 a.download = file.name;
187 document.body.appendChild(a);
188 a.click();
189 window.URL.revokeObjectURL(url);
190 document.body.removeChild(a);
191 }).catch(error => {
192 console.error('There was a problem with the fetch operation:', error);
193 });
194 };
195 thumbDiv.appendChild(label);
196
197 galleryGrid.appendChild(thumbDiv);
198 }
199
200 const handleClick = (file) => {
201 instance.UI.loadDocument(file.url, { filename: file.name, extension: file.extension });
202 };
203
204 return galleryGrid;
205 };
206
207 static createUIControls = (instance) => {
208 // Create a container for all controls
209 const controlsContainer = document.createElement('div');
210 controlsContainer.className = 'button-container';
211
212 // Add the extract button and the legend to the controls container
213 controlsContainer.appendChild(this.extractKeyValuePairsButton(instance));
214 controlsContainer.appendChild(this.colorLegend());
215
216 // Insert the JSON code block and controls container into the playground viewer element (top bar)
217 const playgroundViewerElement = document.getElementById('playground-viewer');
218 playgroundViewerElement.insertBefore(this.jsonElement(), playgroundViewerElement.firstChild);
219 playgroundViewerElement.insertBefore(controlsContainer, playgroundViewerElement.firstChild);
220
221 // Create a gallery container
222 const galleryContainer = document.createElement('div');
223 galleryContainer.className = 'gallery-container';
224 galleryContainer.appendChild(this.galleryPicker(instance));
225
226 // Add the gallery container to the viewer element (left side)
227 const viewerElement = document.getElementById('viewer');
228 viewerElement.style.display = 'flex';
229 viewerElement.style.flexDirection = 'row';
230 viewerElement.insertBefore(galleryContainer, viewerElement.firstChild);
231 };
232}
1/* CSS standards Compliant Syntax */
2/* GitHub Copilot v1.0, Claude Sonnet 4, October 22, 2025 */
3/* File: index.css */
4
5/* Button Styles */
6.btn {
7 display: flex;
8 align-items: center;
9 justify-content: center;
10 background-color: #007bff;
11 margin: 10px;
12 padding: 5px 10px;
13 border: 1px solid #ccc;
14 border-radius: 4px;
15 cursor: pointer;
16 font-size: 14px;
17 font-weight: bold;
18 transition: all 0.2s ease;
19 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
20 color: white;
21 width: 240px;
22}
23
24.btn:hover {
25 background-color: #0056b3;
26 transform: translateY(-1px);
27 box-shadow: 0 4px 8px rgba(0,0,0,0.2);
28}
29
30.btn:active {
31 transform: translateY(1px);
32 box-shadow: 0 1px 2px rgba(0,0,0,0.2);
33}
34
35.btn:disabled {
36 opacity: 0.4;
37 cursor: not-allowed;
38 box-shadow: none;
39 background-color: #E7EBEE;
40 color: #ADB5BD;
41}
42
43.btn .spinner {
44 display: none;
45 border-top: 2px solid currentColor;
46 border-right: 2px solid currentColor;
47 border-bottom-style: solid;
48 border-left-style: solid;
49 border-radius: 99999px;
50 border-bottom-width: 2px;
51 border-left-width: 2px;
52 border-bottom-color: transparent;
53 border-left-color: transparent;
54 animation: rotateBorder 0.45s linear 0s infinite;
55 width: 1em;
56 height: 1em;
57}
58
59@keyframes rotateBorder {
60 0% {
61 transform: rotate(0deg);
62 }
63 100% {
64 transform: rotate(365deg);
65 }
66}
67
68/* Button Container */
69.button-container {
70 display: flex;
71 flex-direction: row;
72 align-items: center;
73 gap: 15px;
74 margin: 5px 0;
75 padding: 16px;
76 padding-bottom: 5px;
77 border-bottom: 1px solid #DFE1E6;
78 background-color: rgba(112, 198, 255, 0.2);
79}
80
81/* JSON Display Container */
82.json-pre {
83 height: 90%;
84 font-family: monospace;
85 white-space: pre-wrap;
86 display: block;
87 overflow: scroll;
88 background-color: #f1f3f5;
89}
90
91.json-wrapper {
92 width: 100%;
93 max-width: 100%;
94 box-sizing: border-box;
95}
96
97.json-container {
98 display: flex;
99 min-height: 140px;
100 max-height: 200px;
101 width: 100%;
102 max-width: 100%;
103 border-radius: 2px;
104 border: 1px solid rgba(0, 0, 0, 0.12);
105 overflow-y: auto;
106 overflow-x: auto;
107 flex-grow: 1;
108 position: relative;
109 padding-bottom: 2px;
110 background-color: rgb(244, 245, 247);
111 box-sizing: border-box;
112}
113
114.json-container .json-pre {
115 font-family: monospace;
116 white-space: pre-wrap;
117 width: 100%;
118 max-width: 100%;
119 margin: 0;
120 padding: 8px;
121 box-sizing: border-box;
122 overflow-wrap: break-word;
123}
124
125#json-code {
126 width: 100%;
127 max-width: 100%;
128 display: block;
129 box-sizing: border-box;
130 background: transparent;
131 border: none;
132 outline: none;
133 resize: none;
134 font-family: inherit;
135}
136
137/* Legend Container */
138.legend-container {
139 display: none;
140 flex-direction: row;
141 gap: 10px;
142}
143
144.legend-item {
145 display: flex;
146 align-items: center;
147 gap: 5px;
148}
149
150.color-box {
151 display: inline-block;
152 width: 16px;
153 height: 16px;
154 border: 1px solid #ccc;
155 border-radius: 3px;
156}
157
158/* Gallery Container on the left */
159.gallery-container {
160 width: 300px;
161 max-width: 300px;
162 display: flex;
163 flex-direction: column;
164 align-items: flex-start;
165 border-right: 1px solid #e0e0e0;
166 background-color: rgba(112, 198, 255, 0.2);
167 transition: margin 0.25s ease-in-out, opacity 0.25s ease-in-out;
168 height: 100%;
169}
170
171/* Gallery Picker Styles */
172.gallery-grid {
173 display: grid;
174 margin: 8px;
175 margin-left: 8px;
176}
177
178.gallery-thumb {
179 display: flex;
180 align-items: center;
181 flex-direction: column;
182 margin-top: 10px;
183 cursor: pointer;
184}
185
186.gallery-thumb-img {
187 border-radius: 6px;
188 height: 140px;
189 max-width: 235px;
190 box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1), 0 3px 5px 0 rgba(0, 0, 0, 0.1);
191 transition: transform 0.1s ease;
192}
193
194.gallery-thumb-img:hover,
195.gallery-thumb-img:focus {
196 transform: scale(1.05);
197 border: 1px solid;
198 border-color: #0206a8;
199}
200
201.gallery-thumb-label {
202 font-size: 0.875rem;
203 line-height: 20px;
204 color: #0206A8;
205 letter-spacing: -0.3px;
206}
207
208/* Responsive Design */
209@media (max-width: 768px) {
210 #viewer {
211 flex-direction: column;
212 }
213
214 .gallery-container {
215 width: 150px;
216 height: auto;
217 border-right: none;
218 border-bottom: 1px solid #e0e0e0;
219 align-items: center;
220 }
221}
222
223
1{
2 "name": "cad-viewer-server",
3 "version": "1.0.0",
4 "description": "CAD Viewer Demo Server Component",
5 "main": "server.js",
6 "scripts": {
7 "start": "node server.js",
8 "dev": "node server.js"
9 },
10 "dependencies": {
11 "@pdftron/data-extraction": "^11.8.0",
12 "@pdftron/pdfnet-node": "^11.8.0",
13 "body-parser": "^1.20.2",
14 "express": "^4.18.2",
15 "multer": "^1.4.4",
16 "open": "^9.1.0"
17 },
18 "keywords": [
19 "cad-viewer",
20 "pdf",
21 "server",
22 "pdftron",
23 "webviewer"
24 ],
25 "author": "Apryse",
26 "license": "MIT"
27}
28
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales