Extracts key-value pairs from CAD drawings in PDF format with well-structured title blocks. Outputs the data as JSON and adds visual annotations to the document to illustrate the relationships.
This demo allows you to:
If starting with a CAD file, we can also help you convert that to PDF. Please check out our CAD File Conversion Sample Code for more.
To add CAD Title Block Data Extraction capability with WebViewer:
Step 1: Get started with WebViewer in your preferred web stack.
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, October 22, 2025
3// File: index.js
4
5import WebViewer from '@pdftron/webviewer';
6
7const licenseKey = 'YOUR_WEBVIEWER_LICENSE_KEY';
8
9// CAD Title Block Data Extraction Demo
10//
11// This code demonstrates how to extract key-value data pairs from CAD drawings with well formed title blocks
12//
13// **Important**
14// 1. You must get a license key from Apryse for the server to run.
15// A trial key can be obtained from:
16// https://docs.apryse.com/core/guides/get-started/trial-key
17//
18// 2. You need to also run the `npm install` command at /title-block-data-extraction/server/ location to install the `@pdftron/pdfnet-node`, `@pdftron/cad`, and `@pdftron/data-extraction` packages.
19
20function initializeWebViewer() {
21
22 // This code initializes the WebViewer with the basic settings
23 WebViewer({
24 path: '/lib',
25 licenseKey: licenseKey,
26 }, document.getElementById('viewer')).then((instance) => {
27 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool
28 const cloudyTools = [
29 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
30 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
31 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
32 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
33 ];
34 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
35 instance.UI.disableTools(cloudyTools);
36 // Set default toolbar group to Annotate
37 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
38 // Set default tool on mobile devices to Pan.
39 if (UIElements.isMobileDevice()) {
40 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
41 }
42
43 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
44 if (searchParams.has('file')) {
45 searchParams.delete('file');
46 history.replaceState(null, '', '?' + searchParams.toString());
47 }
48 });
49
50 instance.Core.annotationManager.enableAnnotationNumbering();
51 instance.UI.NotesPanel.enableAttachmentPreview();
52 // Add the demo-specific functionality
53 customizeUI(instance).then(() => {
54 // Create UI controls after demo is initialized
55 UIElements.createUIControls(instance);
56 });
57 });
58}
59
60const searchParams = new URLSearchParams(globalThis.location.search);
61const history = globalThis.history || globalThis.parent?.history || globalThis.top?.history;
62
63// Starting page for extraction
64let startPage = 1;
65
66// Global variable to hold result data
67globalThis.resultData = null;
68
69const defaultDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/cad_floor_plan.pdf';
70
71const customizeUI = async (instance) => {
72 // Customize the UI for the title-block-data-extraction demo
73 instance.UI.setToolbarGroup('toolbarGroup-View');
74 instance.UI.disableElements(['thumbnailControl']);
75
76 // Reset variables when new document is loaded
77 instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
78 globalThis.resultData = null;
79 startPage = 1;
80
81 // Reset the JSON display area and Color Legend
82 UIElements.resetUI(instance);
83 });
84
85 // Load the default CAD file for demonstration
86 if (defaultDoc) {
87 await loadCadDocument(instance, defaultDoc);
88 }
89};
90
91// Load a CAD document, converting to PDF if necessary
92const loadCadDocument = async (instance, cadUrl) => {
93 // Get the file name and extension
94 const cadFilename = cadUrl.split('/').pop();
95 const extension = cadFilename.split('.').pop().toLowerCase();
96
97 console.log(`Preparing to load document: ${cadFilename}`);
98 console.log(`File extension detected: ${extension}`);
99
100 // If the file is a CAD format, convert it to PDF first
101 if (cadUrl && extension) {
102 if (['dwg', 'dxf', 'dgn', 'rvt'].includes(extension)) {
103 console.log(`Loading CAD file: ${cadFilename}`);
104 const response = await fetch(cadUrl);
105 if (!response.ok) {
106 throw new Error(`Failed to fetch CAD: ${response.status}`);
107 }
108 const cadBuffer = await response.arrayBuffer();
109 const pdfBuffer = await convertCadtoPdf(cadBuffer, cadFilename);
110 instance.UI.loadDocument(pdfBuffer, {
111 extension: 'pdf',
112 });
113 } else {
114 console.log(`Loading document: ${cadFilename}`);
115 instance.UI.loadDocument(cadUrl);
116 }
117 }
118};
119
120// Function to convert CAD ArrayBuffer to PDF ArrayBuffer via server
121const convertCadtoPdf = async (cadBuffer, cadFilename) => {
122 // Send the CAD to the server to be converted to PDF
123 console.log('Sending CAD to server for conversion...');
124 const cadBlob = new Blob([cadBuffer]);
125 const formData = new FormData();
126 formData.append('cadfile', cadBlob, cadFilename);
127
128 const postResponse = await fetch('http://localhost:5050/server/handler.js', {
129 method: 'POST',
130 body: formData,
131 });
132
133 if (postResponse.status !== 200) {
134 throw new Error(`Server error during CAD upload: ${postResponse.status}`);
135 }
136 const buffer = await postResponse.arrayBuffer();
137 return buffer;
138};
139globalThis.convertCadtoPdf = convertCadtoPdf; // Make convertCadtoPdf globally available so that the UIElements module can access it
140
141// Function to extract key-value pairs from title block via server
142const extractKeyValuePairs = async (instance) => {
143 const doc = instance.Core.documentViewer.getDocument();
144 if (doc) {
145 const pdfBuffer = await doc.getFileData({ flags: instance.Core.SaveOptions.LINEARIZED });
146 console.log('Sending PDF to server for key-value extraction...');
147 const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' });
148 const formData = new FormData();
149 formData.append('pdffile', pdfBlob, 'viewerDocument.pdf');
150
151 // Send the PDF to the server to extract key-value pairs
152 const postResponse = await fetch('http://localhost:5050/server/handler.js/extract-key-value-pairs', {
153 method: 'POST',
154 body: formData,
155 });
156
157 if (postResponse.status !== 200) {
158 throw new Error(`Server error during PDF upload: ${postResponse.status}`);
159 }
160
161 // Retrieve and parse the JSON response
162 const jsonResponse = await postResponse.json();
163 const docStructureData = JSON.parse(jsonResponse);
164 globalThis.resultData = JSON.stringify(docStructureData, null, 2);
165
166 // Draw annotations on the document based on extracted data
167 drawAnnotations(docStructureData, instance);
168 }
169}
170globalThis.extractKeyValuePairs = extractKeyValuePairs; // Make extractKeyValuePairs globally available so that the UIElements module can access it
171
172// Function to draw annotations on the document based on extracted key-value data
173const drawAnnotations = (docStructureData, instance) => {
174 const { annotationManager, Annotations } = instance.Core;
175
176 // Retrieve the first page's data
177 const page = docStructureData.pages[startPage - 1];
178 const pageNumber = page?.properties?.pageNumber;
179 console.log(`Processing Page ${pageNumber} for annotations...`);
180 for (const kv of page.keyValueElements ?? []) {
181 const valueRect = kv?.rect;
182 const keyRect = kv?.key?.rect;
183 const hasValueWords = (kv?.words?.length ?? 0) > 0;
184
185 // Only draw if value has words
186 if (!hasValueWords) continue;
187
188 // value: blue
189 const valueAnnot = new Annotations.RectangleAnnotation({
190 PageNumber: pageNumber,
191 X: valueRect[0],
192 Y: valueRect[1],
193 Width: valueRect[2] - valueRect[0],
194 Height: valueRect[3] - valueRect[1],
195 StrokeColor: new Annotations.Color(0, 0, 255),
196 StrokeThickness: 1,
197 });
198 annotationManager.addAnnotation(valueAnnot);
199 annotationManager.redrawAnnotation(valueAnnot);
200
201 // key: red
202 const keyAnnot = new Annotations.RectangleAnnotation({
203 PageNumber: pageNumber,
204 X: keyRect[0],
205 Y: keyRect[1],
206 Width: keyRect[2] - keyRect[0],
207 Height: keyRect[3] - keyRect[1],
208 StrokeColor: new Annotations.Color(255, 0, 0),
209 StrokeThickness: 1,
210 });
211 annotationManager.addAnnotation(keyAnnot);
212 annotationManager.redrawAnnotation(keyAnnot);
213
214 // Green connector
215 const line = new Annotations.LineAnnotation();
216 line.pageNumber = pageNumber;
217 line.StrokeColor = new Annotations.Color(0, 255, 0);
218 line.StrokeThickness = 1;
219 line.Start = topLeftPoint(valueRect, instance);
220 line.End = topLeftPoint(keyRect, instance);
221 annotationManager.addAnnotation(line);
222 annotationManager.redrawAnnotation(line);
223 }
224};
225
226// Helper function to get top-left point of a rectangle
227const topLeftPoint = ([x1, y1, x2, y2], instance) => {
228 return new instance.Core.Math.Point(Math.min(x1, x2), Math.min(y1, y2));
229};
230
231// Cleanup function for when the demo is closed or page is unloaded
232const cleanup = (instance) => {
233 if (instance !== undefined && instance.UI) {
234 if (instance.Core.documentViewer.getDocument()) {
235 // Insert any other cleanup code here
236 }
237 console.log('Cleaning up title-block-data-extraction demo');
238 }
239};
240
241// Register cleanup for page unload
242globalThis.addEventListener('beforeunload', () => cleanup(instance));
243globalThis.addEventListener('unload', () => cleanup(instance));
244
245// Helper function to load the ui-elements.js script
246function loadUIElementsScript() {
247 return new Promise((resolve, reject) => {
248 if (globalThis.UIElements) {
249 console.log('UIElements already loaded');
250 resolve();
251 return;
252 }
253 const script = document.createElement('script');
254 script.src = '/showcase-demos/title-block-extraction/client/ui-elements.js';
255 script.onload = function () {
256 console.log('✅ UIElements script loaded successfully');
257 resolve();
258 };
259 script.onerror = function () {
260 console.error('Failed to load UIElements script');
261 reject(new Error('Failed to load ui-elements.js'));
262 };
263 document.head.appendChild(script);
264 });
265}
266
267// Load UIElements script first, then initialize WebViewer
268loadUIElementsScript().then(() => {
269 initializeWebViewer();
270}).catch((error) => {
271 console.error('Failed to load UIElements:', error);
272});
273
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: #0056b3;
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: #ffffff;
21 width: 240px;
22}
23
24.btn:hover {
25 background-color: #003d80;
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: #495057;
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}
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, Claude Sonnet 4, October 22, 2025
3// File: ui-elements.js
4
5// UI Elements class to create and manage custom UI controls
6//
7// Helper code to add controls to the viewer holding the buttons
8// This code creates a container for the buttons, styles them, and adds them to the viewer
9//
10class UIElements {
11
12 // Function to check if the user is on a mobile device
13 static isMobileDevice = () => {
14 const ua = globalThis.navigator.userAgent;
15 const uaShort = ua.substring(0, 4);
16 return (
17 // UA string patterns (split for complexity)
18 /android.*mobile|avantgo|bada\/|blackberry|blazer/i.test(ua) ||
19 /compal|elaine|fennec|hiptop|iemobile|ip(hone|od)/i.test(ua) ||
20 /ipad|iris|kindle|silk|lge |maemo|midp|mmp/i.test(ua) ||
21 /netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\//i.test(ua) ||
22 /plucker|pocket|psp|series[46]0|symbian|treo/i.test(ua) ||
23 /up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(ua) ||
24 // 4-char prefix patterns (split for complexity, single-char alternations use character classes)
25 /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s/i.test(uaShort) ||
26 /a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)/i.test(uaShort) ||
27 /amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)/i.test(uaShort) ||
28 /attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)/i.test(uaShort) ||
29 /bl(ac|az)|br[ev]w|bumb|bw-[nu]|c55\/|capi|ccwa/i.test(uaShort) ||
30 /cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw/i.test(uaShort) ||
31 /da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do[cp]o/i.test(uaShort) ||
32 /ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8/i.test(uaShort) ||
33 /ez([4-7]0|os|wa|ze)|fetc|fly[-_]|g1 u|g560|gene/i.test(uaShort) ||
34 /gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit/i.test(uaShort) ||
35 /hd-[mpt]|hei-|hi(pt|ta)|hp( i|ip)|hs-c/i.test(uaShort) ||
36 /ht(c[- _agpst]|tp)|hu(aw|tc)|i-(20|go|ma)|i230/i.test(uaShort) ||
37 /iac[ \-/]|ibro|idea|ig01|ikom|im1k|inno|ipaq/i.test(uaShort) ||
38 /iris|ja[tv]a|jbro|jemu|jigs|kddi|keji/i.test(uaShort) ||
39 /kgt[ /]|klon|kpt |kwc-|kyo[ck]|le(no|xi)/i.test(uaShort) ||
40 /lg( g|\/[klu]|50|54|-[a-w])|libw|lynx|m1-w/i.test(uaShort) ||
41 /m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr/i.test(uaShort) ||
42 /me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t[- ov]|zz)/i.test(uaShort) ||
43 /mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30[02]/i.test(uaShort) ||
44 /n50[025]|n7(0[01]|10)|ne([cm]-|on|tf|wf|wg|wt)/i.test(uaShort) ||
45 /nok[6i]|nzph|o2im|op(ti|wv)|oran|owg1|p800/i.test(uaShort) ||
46 /pan[adt]|pdxg|pg(13|-[1-8c])|phil|pire|pl(ay|uc)/i.test(uaShort) ||
47 /pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a/i.test(uaShort) ||
48 /qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks/i.test(uaShort) ||
49 /rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)/i.test(uaShort) ||
50 /sc(01|h-|oo|p-)|sdk\/|se(c[-01]|47|mc|nd|ri)/i.test(uaShort) ||
51 /sgh-|shar|sie[-m]|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)/i.test(uaShort) ||
52 /so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)/i.test(uaShort) ||
53 /t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel[im]/i.test(uaShort) ||
54 /tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9/i.test(uaShort) ||
55 /up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)/i.test(uaShort) ||
56 /vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)/i.test(uaShort) ||
57 /w3c[- ]|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700/i.test(uaShort) ||
58 /yas-|your|zeto|zte-/i.test(uaShort)
59 );
60 }
61
62 // Choose File button
63 static filePicker = (instance) => {
64 const button = document.createElement('button');
65 button.className = 'btn';
66 button.textContent = 'Choose File';
67 button.onclick = () => {
68 const input = document.createElement('input');
69 input.type = 'file';
70 input.accept = '.dwg,.dxf,.dwf,.dgn,.rvt,.pdf'; // Supported CAD file formats
71 input.onchange = async (event) => {
72 try {
73 const file = event.target.files[0];
74 const extension = file.name.split('.').pop().toLowerCase();
75 if (file && ['dwg', 'dxf', 'dgn', 'rvt'].includes(extension)) {
76 const arrayBuffer = await file.arrayBuffer();
77 const pdfBuffer = await globalThis.convertCadtoPdf(arrayBuffer, file.name);
78 instance.UI.loadDocument(pdfBuffer, {
79 extension: 'pdf',
80 });
81 } else if (file && extension === 'pdf') {
82 instance.UI.loadDocument(file);
83 } else {
84 alert('Unsupported file format. Please select a CAD or PDF file.');
85 }
86 } catch (e) {
87 console.error(e);
88 }
89 };
90 input.click();
91 };
92
93 return button;
94 }
95
96 // JSON Code Block Element
97 static jsonElement = () => {
98 const wrapper = document.createElement('div');
99 wrapper.className = 'json-wrapper';
100 wrapper.style.display = 'none'; // Initially hidden
101
102 // Container for the JSON code block
103 const container = document.createElement('div');
104 container.className = 'json-container';
105
106 // Code block for JSON
107 const codePre = document.createElement('pre');
108 codePre.className = 'json-pre';
109
110 const codeBlock = document.createElement('code');
111 codeBlock.id = 'json-code';
112 codeBlock.contentEditable = false;
113
114 // Assemble the JSON code block
115 codePre.appendChild(codeBlock);
116 container.appendChild(codePre);
117 wrapper.appendChild(container);
118 return wrapper;
119 };
120
121 // Extract Key Value Pairs button
122 static extractKeyValuePairsButton = (instance) => {
123 // Spinner element to indicate loading
124 const spinner = document.createElement('div');
125 spinner.className = 'spinner';
126
127 // Button element for extracting key-value pairs
128 const button = document.createElement('button');
129 button.className = 'btn extract-btn';
130 button.textContent = 'Extract Key-Value Pairs';
131 button.onclick = async () => {
132 try {
133 button.disabled = true;
134 spinner.style.display = 'inline-block';
135 await globalThis.extractKeyValuePairs(instance);
136 } catch (e) {
137 console.error(e);
138 button.disabled = false;
139 }
140 finally {
141 // Hide spinner when done
142 spinner.style.display = 'none';
143
144 // Display the extracted JSON data
145 const jsonWrapper = document.querySelector('.json-wrapper');
146 const jsonCodeBlock = document.getElementById('json-code');
147 if (globalThis.resultData) {
148 jsonCodeBlock.textContent = JSON.stringify(JSON.parse(globalThis.resultData), null, 2);
149 jsonWrapper.style.display = 'flex';
150 } else {
151 jsonCodeBlock.textContent = '';
152 jsonWrapper.style.display = 'none';
153 }
154
155 // Show color legend if extraction was successful
156 const legendContainer = document.querySelector('.legend-container');
157 if (globalThis.resultData) {
158 legendContainer.style.display = 'flex';
159 } else {
160 legendContainer.style.display = 'none';
161 }
162 }
163 };
164
165 button.appendChild(spinner);
166
167 return button;
168 }
169
170 // Legends for the Annotations
171 static colorLegend = () => {
172 const legendContainer = document.createElement('div');
173 legendContainer.className = 'legend-container';
174
175 const colors = ['rgb(255, 0, 0)', 'rgb(0, 0, 255)', 'rgb(0, 255, 0)'];
176 const labels = ['Key', 'Value', 'Connector'];
177
178 for (let i = 0; i < colors.length; i++) {
179 const legendItem = document.createElement('div');
180 legendItem.className = 'legend-item';
181
182 const colorBox = document.createElement('span');
183 colorBox.className = 'color-box';
184 colorBox.style.backgroundColor = colors[i];
185
186 const label = document.createElement('span');
187 label.textContent = labels[i];
188
189 legendItem.appendChild(colorBox);
190 legendItem.appendChild(label);
191 legendContainer.appendChild(legendItem);
192 }
193
194 return legendContainer;
195 }
196
197 // Reset JSON Code Block and Legend
198 static resetUI = () => {
199 // Hide JSON code block
200 const jsonWrapper = document.querySelector('.json-wrapper');
201 const jsonCodeBlock = document.getElementById('json-code');
202 jsonCodeBlock.textContent = '';
203 jsonWrapper.style.display = 'none';
204
205 // Hide legend
206 const legendContainer = document.querySelector('.legend-container');
207 legendContainer.style.display = 'none';
208
209 // Enable Extract button
210 const extractButton = document.querySelector('.extract-btn');
211 extractButton.disabled = false;
212 }
213
214 static createUIControls = (instance) => {
215 // Create a container for all controls
216 const controlsContainer = document.createElement('div');
217 controlsContainer.className = 'button-container';
218
219 // Add the file picker and Import/Export buttons to the controls container
220 controlsContainer.appendChild(this.filePicker(instance));
221 controlsContainer.appendChild(this.extractKeyValuePairsButton(instance));
222 controlsContainer.appendChild(this.colorLegend());
223
224 // Add the controls container to the viewer element
225 const element = document.getElementById('viewer');
226 element.insertBefore(this.jsonElement(), element.firstChild);
227 element.insertBefore(controlsContainer, element.firstChild);
228 };
229}
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, Claude Sonnet 4, October 22, 2025
3// File: handler.js
4// This file will handle CAD file conversion and extraction requests.
5
6const fs = require('node: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 /cad-viewer/server/ location to install the `@pdftron/pdfnet-node` and `@pdftron/cad` packages.
15const licenseKey = 'YOUR_SERVER_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 function handler(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 CAD format 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 PDFTron CAD and Data Extraction library path
45 await PDFNet.addResourceSearchPath('./node_modules/@pdftron/cad/lib/');
46 await PDFNet.addResourceSearchPath('./node_modules/@pdftron/data-extraction/lib/');
47
48 // Check if the Apryse SDK CAD module is available.
49 if (await PDFNet.CADModule.isModuleAvailable())
50 console.log('Apryse SDK CAD module is available.');
51 else
52 console.log('Unable to run: Apryse SDK CAD module not available.');
53
54 // Check if the Apryse SDK Data Extraction module is available.
55 if (await PDFNet.DataExtractionModule.isModuleAvailable(PDFNet.DataExtractionModule.DataExtractionEngine.e_GenericKeyValue))
56 console.log('Apryse SDK Data Extraction module is available.');
57 else
58 console.log('Unable to run: Apryse SDK Data Extraction module not available.');
59 }
60
61 // Handle POST request sent to '/server/handler.js'
62 // This endpoint receives the CAD file URL to be loaded in the Apryse webviewer, then saves it to the server
63 app.post(serverHandler, upload.single('cadfile'), async (request, response) => {
64 try {
65 const cadFilename = request.file.originalname;
66 const fullFilename = request.file.path;
67
68 // Convert the CAD file to PDF and get the buffer
69 const buffer = await convertCadToPdfBuffer(fullFilename);
70 console.log(`Conversion complete, extracting title block data...`);
71
72 // Set headers to indicate a PDF file attachment and send the buffer
73 await response.setHeader('Content-Type', 'application/pdf');
74 await response.setHeader('Content-Disposition', `attachment; filename="${cadFilename.replace(/\.[^/.]+$/, ".pdf")}"`);
75 response.status(200).send(buffer);
76 } catch (e) {
77 response.status(500).send(`Error processing CAD file: ${e.message}`);
78 } finally {
79 // Cleanup: remove the sent CAD file
80 const cadPath = request.file.path;
81 fs.unlink(cadPath, (err) => {
82 if (err) {
83 console.error(`Error removing CAD file ${cadPath}: ${err.message}`);
84 }
85 });
86 }
87 });
88
89 // Function to convert CAD file to PDF and return as buffer
90 const convertCadToPdfBuffer = async (fullFilename) => {
91 try {
92 // Create a new PDF document and convert the CAD file to PDF
93 const doc = await PDFNet.PDFDoc.create();
94 console.log('Converting CAD to PDF. Filename and Extension:', fullFilename);
95
96 const options = new PDFNet.Convert.CADConvertOptions();
97 options.setPageWidth(800);
98 options.setPageHeight(600);
99 options.setRasterDPI(150);
100
101 await PDFNet.Convert.fromCAD(doc, fullFilename, options);
102
103 // Initialize security handler and lock the document
104 doc.initSecurityHandler();
105 doc.lock();
106
107 // Save the PDF document to a memory buffer
108 console.log('After Conversion and Stored in PDFDoc Full filename:', doc.fullFilename);
109 const uint8Array = await doc.saveMemoryBuffer(PDFNet.SDFDoc.SaveOptions.e_linearized);
110 const buffer = Buffer.from(uint8Array);
111
112 // Unlock the document
113 doc.unlock();
114
115 // Return the PDF buffer
116 return buffer;
117 }
118 catch (err) {
119 console.log(err);
120 throw new Error(err);
121 }
122 };
123
124 // Handle POST request sent to '/server/handler.js/extract-key-value-pairs'
125 // This endpoint receives the PDF file path, extracts key-value data from the title block, and returns it as JSON
126 app.post(`${serverHandler}/extract-key-value-pairs`, upload.single('pdffile'), async (request, response) => {
127 try {
128 console.log('Received PDF for key-value extraction');
129 const pdfPath = request.file.path;
130 const jsonResponse = await extractKeyValuePairs(pdfPath);
131 response.status(200).json(jsonResponse);
132 } catch (error) {
133 console.error('Error extracting key-value data:', error);
134 response.status(500).send('Error extracting key-value data');
135 } finally {
136 // Cleanup: remove the sent PDF file
137 const pdfPath = request.file.path;
138 fs.unlink(pdfPath, (err) => {
139 if (err) {
140 console.error(`Error removing PDF file ${pdfPath}: ${err.message}`);
141 }
142 });
143 }
144 });
145
146 // Function to extract key-value pairs from PDF using Data Extraction module
147 const extractKeyValuePairs = async (pdf) => {
148 try {
149 // Set up data extraction options
150 const options = new PDFNet.DataExtractionModule.DataExtractionOptions();
151 console.log('Setting extraction language to English');
152 options.setLanguage('eng');
153
154 // Extract key-value data from the PDF using the provided JSON template
155 const jsonString = await PDFNet.DataExtractionModule.extractDataAsString(pdf, PDFNet.DataExtractionModule.DataExtractionEngine.e_GenericKeyValue, options);
156 return jsonString;
157
158 } catch (err) {
159 console.log(err);
160 throw new Error(err);
161 }
162 };
163
164 // Initialize PDFNet
165 PDFNet.runWithoutCleanup(initializePDFNet, licenseKey).then(
166 function onFulfilled() {
167 response.status(200);
168 },
169 function onRejected(error) {
170 // log error and close response
171 console.error('Error initializing PDFNet', error);
172 response.status(503).send();
173 }
174 );
175};
176
1// ES6 Compliant Syntax
2// GitHub Copilot v1.0, Claude Sonnet 4, October 22, 2025
3// File: server.js
4// This file is to run a server in localhost.
5
6const express = require('express');
7const fs = require('node: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
43// Server shutdown and cleanup
44function shutdown() {
45 console.log('Cleanup started...');
46
47 // Example: Close server
48 server.close(() => {
49 console.log('Server closed.');
50
51 // Removes sent PDFs folder
52 if (fs.existsSync(sentPdfs))
53 fs.rmdirSync(sentPdfs, { recursive: true });
54
55 // If no async cleanup, exit directly
56 process.exit(0);
57 });
58}
59
60// Handle shutdown signals
61process.on('SIGINT', shutdown); // Ctrl+C
62process.on('SIGTERM', shutdown); // kill command or Docker stop
63process.on('uncaughtException', (err) => {
64 console.error('Uncaught Exception:', err);
65 shutdown();
66});
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/cad": "^11.8.0",
12 "@pdftron/data-extraction": "^11.8.0",
13 "@pdftron/pdfnet-node": "^11.8.0",
14 "body-parser": "^1.20.2",
15 "express": "^4.18.2",
16 "multer": "^1.4.4",
17 "open": "^9.1.0"
18 },
19 "keywords": [
20 "cad-viewer",
21 "pdf",
22 "server",
23 "pdftron",
24 "webviewer"
25 ],
26 "author": "Apryse",
27 "license": "MIT"
28}
29
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales