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
7// CAD Title Block Data Extraction Demo
8//
9// This code demonstrates how to extract key-value data pairs from CAD drawings with well formed title blocks.
10//
11// **Important**
12// 1. You must get a license key from Apryse for the server to run.
13// A trial key can be obtained from:
14// https://docs.apryse.com/core/guides/get-started/trial-key
15//
16// 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.
17
18function initializeWebViewer() {
19
20 // This code initializes the WebViewer with the basic settings.
21 WebViewer({
22 path: '/lib',
23 licenseKey: 'YOUR_LICENSE_KEY',
24 }, document.getElementById('viewer')).then((instance) => {
25 // Enable the measurement toolbar so it appears with all the other tools, and disable Cloudy rectangular tool.
26 const cloudyTools = [
27 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT,
28 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT2,
29 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT3,
30 instance.Core.Tools.ToolNames.CLOUDY_RECTANGULAR_AREA_MEASUREMENT4,
31 ];
32 instance.UI.enableFeatures([instance.UI.Feature.Measurement, instance.UI.Feature.Initials]);
33 instance.UI.disableTools(cloudyTools);
34 // Set default toolbar group to Annotate.
35 instance.UI.setToolbarGroup('toolbarGroup-Annotate');
36 // Set default tool on mobile devices to Pan.
37 if (UIElements.isMobileDevice()) {
38 instance.UI.setToolMode(instance.Core.Tools.ToolNames.PAN);
39 }
40
41 instance.Core.documentViewer.addEventListener('documentUnloaded', () => {
42 if (searchParams.has('file')) {
43 searchParams.delete('file');
44 history.replaceState(null, '', '?' + searchParams.toString());
45 }
46 });
47
48 instance.Core.annotationManager.enableAnnotationNumbering();
49 instance.UI.NotesPanel.enableAttachmentPreview();
50 // Add the demo-specific functionality.
51 customizeUI(instance).then(() => {
52 // Create UI controls after demo is initialized.
53 UIElements.createUIControls(instance);
54 });
55 });
56}
57
58const searchParams = new URLSearchParams(window.location.search);
59const history = window.history || window.parent.history || window.top.history;
60
61// Starting page for extraction.
62let startPage = 1;
63
64// Global variable to hold result data.
65window.resultData = null;
66
67const defaultDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/cad_floor_plan.pdf';
68
69const customizeUI = async (instance) => {
70 // Customize the UI for the title-block-data-extraction demo.
71 instance.UI.setToolbarGroup('toolbarGroup-View');
72 instance.UI.disableElements(['thumbnailControl']);
73
74 // Reset variables when new document is loaded.
75 instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
76 window.resultData = null;
77 startPage = 1;
78
79 // Reset the JSON display area and Color Legend.
80 UIElements.resetUI(instance);
81 });
82
83 // Load the default CAD file for demonstration.
84 if (defaultDoc) {
85 await loadCadDocument(instance, defaultDoc);
86 }
87};
88
89// Load a CAD document, converting to PDF if necessary.
90const loadCadDocument = async (instance, cadUrl) => {
91 // Get the file name and extension.
92 const cadFilename = cadUrl.split('/').pop();
93 const extension = cadFilename.split('.').pop().toLowerCase();
94
95 console.log(`Preparing to load document: ${cadFilename}`);
96 console.log(`File extension detected: ${extension}`);
97
98 // If the file is a CAD format, convert it to PDF first.
99 if (cadUrl && extension) {
100 if (['dwg', 'dxf', 'dgn', 'rvt'].includes(extension)) {
101 console.log(`Loading CAD file: ${cadFilename}`);
102 const response = await fetch(cadUrl);
103 if (!response.ok) {
104 throw new Error(`Failed to fetch CAD: ${response.status}`);
105 }
106 const cadBuffer = await response.arrayBuffer();
107 const pdfBuffer = await convertCadtoPdf(cadBuffer, cadFilename);
108 instance.UI.loadDocument(pdfBuffer, {
109 extension: 'pdf',
110 });
111 } else {
112 console.log(`Loading document: ${cadFilename}`);
113 instance.UI.loadDocument(cadUrl);
114 }
115 }
116};
117
118// Function to convert CAD ArrayBuffer to PDF ArrayBuffer via server.
119const convertCadtoPdf = async (cadBuffer, cadFilename) => {
120 // Send the CAD to the server to be converted to PDF.
121 console.log('Sending CAD to server for conversion...');
122 const cadBlob = new Blob([cadBuffer]);
123 const formData = new FormData();
124 formData.append('cadfile', cadBlob, cadFilename);
125
126 const postResponse = await fetch('http://localhost:5050/server/handler.js', {
127 method: 'POST',
128 body: formData,
129 });
130
131 if (postResponse.status !== 200) {
132 throw new Error(`Server error during CAD upload: ${postResponse.status}`);
133 }
134 const buffer = await postResponse.arrayBuffer();
135 return buffer;
136};
137window.convertCadtoPdf = convertCadtoPdf; // Make convertCadtoPdf globally available so that the UIElements module can access it.
138
139// Function to extract key-value pairs from title block via server.
140const extractKeyValuePairs = async (instance) => {
141 const doc = instance.Core.documentViewer.getDocument();
142 if (doc) {
143 const pdfBuffer = await doc.getFileData({ flags: instance.Core.SaveOptions.LINEARIZED });
144 console.log('Sending PDF to server for key-value extraction...');
145 const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' });
146 const formData = new FormData();
147 formData.append('pdffile', pdfBlob, 'viewerDocument.pdf');
148
149 // Send the PDF to the server to extract key-value pairs.
150 const postResponse = await fetch('http://localhost:5050/server/handler.js/extract-key-value-pairs', {
151 method: 'POST',
152 body: formData,
153 });
154
155 if (postResponse.status !== 200) {
156 throw new Error(`Server error during PDF upload: ${postResponse.status}`);
157 }
158
159 // Retrieve and parse the JSON response.
160 const jsonResponse = await postResponse.json();
161 const docStructureData = JSON.parse(jsonResponse);
162 resultData = JSON.stringify(docStructureData, null, 2);
163
164 // Draw annotations on the document based on extracted data.
165 drawAnnotations(docStructureData, instance);
166
167 return;
168 }
169}
170window.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 (typeof 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.
242window.addEventListener('beforeunload', () => cleanup(instance));
243window.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 (window.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-data-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: #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}
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 return (
15 /(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(
16 window.navigator.userAgent
17 ) ||
18 /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(
19 window.navigator.userAgent.substring(0, 4)
20 )
21 );
22 }
23
24 // Choose File button.
25 static filePicker = (instance) => {
26 const button = document.createElement('button');
27 button.className = 'btn';
28 button.textContent = 'Choose File';
29 button.onclick = () => {
30 const input = document.createElement('input');
31 input.type = 'file';
32 input.accept = '.dwg,.dxf,.dwf,.dgn,.rvt,.pdf'; // Supported CAD file formats
33 input.onchange = async (event) => {
34 try {
35 const file = event.target.files[0];
36 const extension = file.name.split('.').pop().toLowerCase();
37 if (file && ['dwg', 'dxf', 'dgn', 'rvt'].includes(extension)) {
38 const arrayBuffer = await file.arrayBuffer();
39 const pdfBuffer = await window.convertCadtoPdf(arrayBuffer, file.name);
40 instance.UI.loadDocument(pdfBuffer, {
41 extension: 'pdf',
42 });
43 } else if (file && extension === 'pdf') {
44 instance.UI.loadDocument(file);
45 } else {
46 alert('Unsupported file format. Please select a CAD or PDF file.');
47 }
48 } catch (e) {
49 console.error(e);
50 }
51 };
52 input.click();
53 };
54
55 return button;
56 }
57
58 // JSON Code Block Element
59 static jsonElement = () => {
60 const wrapper = document.createElement('div');
61 wrapper.className = 'json-wrapper';
62 wrapper.style.display = 'none'; // Initially hidden
63
64 // Container for the JSON code block
65 const container = document.createElement('div');
66 container.className = 'json-container';
67
68 // Code block for JSON
69 const codePre = document.createElement('pre');
70 codePre.className = 'json-pre';
71
72 const codeBlock = document.createElement('code');
73 codeBlock.id = 'json-code';
74 codeBlock.contentEditable = false;
75
76 // Assemble the JSON code block.
77 codePre.appendChild(codeBlock);
78 container.appendChild(codePre);
79 wrapper.appendChild(container);
80 return wrapper;
81 };
82
83 // Extract Key Value Pairs button.
84 static extractKeyValuePairsButton = (instance) => {
85 // Spinner element to indicate loading.
86 const spinner = document.createElement('div');
87 spinner.className = 'spinner';
88
89 // Button element for extracting key-value pairs.
90 const button = document.createElement('button');
91 button.className = 'btn extract-btn';
92 button.textContent = 'Extract Key-Value Pairs';
93 button.onclick = async () => {
94 try {
95 button.disabled = true;
96 spinner.style.display = 'inline-block';
97 await window.extractKeyValuePairs(instance);
98 } catch (e) {
99 console.error(e);
100 button.disabled = false;
101 }
102 finally {
103 // Hide spinner when done.
104 spinner.style.display = 'none';
105
106 // Display the extracted JSON data.
107 const jsonWrapper = document.querySelector('.json-wrapper');
108 const jsonCodeBlock = document.getElementById('json-code');
109 if (window.resultData) {
110 jsonCodeBlock.textContent = JSON.stringify(JSON.parse(window.resultData), null, 2);
111 jsonWrapper.style.display = 'flex';
112 } else {
113 jsonCodeBlock.textContent = '';
114 jsonWrapper.style.display = 'none';
115 }
116
117 // Show color legend if extraction was successful.
118 const legendContainer = document.querySelector('.legend-container');
119 if (window.resultData) {
120 legendContainer.style.display = 'flex';
121 } else {
122 legendContainer.style.display = 'none';
123 }
124 }
125 };
126
127 button.appendChild(spinner);
128
129 return button;
130 }
131
132 // Legends for the Annotations
133 static colorLegend = () => {
134 const legendContainer = document.createElement('div');
135 legendContainer.className = 'legend-container';
136
137 const colors = ['rgb(255, 0, 0)', 'rgb(0, 0, 255)', 'rgb(0, 255, 0)'];
138 const labels = ['Key', 'Value', 'Connector'];
139
140 for (let i = 0; i < colors.length; i++) {
141 const legendItem = document.createElement('div');
142 legendItem.className = 'legend-item';
143
144 const colorBox = document.createElement('span');
145 colorBox.className = 'color-box';
146 colorBox.style.backgroundColor = colors[i];
147
148 const label = document.createElement('span');
149 label.textContent = labels[i];
150
151 legendItem.appendChild(colorBox);
152 legendItem.appendChild(label);
153 legendContainer.appendChild(legendItem);
154 }
155
156 return legendContainer;
157 }
158
159 // Reset JSON Code Block and Legend.
160 static resetUI = () => {
161 // Hide JSON code block.
162 const jsonWrapper = document.querySelector('.json-wrapper');
163 const jsonCodeBlock = document.getElementById('json-code');
164 jsonCodeBlock.textContent = '';
165 jsonWrapper.style.display = 'none';
166
167 // Hide legend.
168 const legendContainer = document.querySelector('.legend-container');
169 legendContainer.style.display = 'none';
170
171 // Enable Extract button.
172 const extractButton = document.querySelector('.extract-btn');
173 extractButton.disabled = false;
174 }
175
176 static createUIControls = (instance) => {
177 // Create a container for all controls.
178 const controlsContainer = document.createElement('div');
179 controlsContainer.className = 'button-container';
180
181 // Add the file picker and Import/Export buttons to the controls container.
182 controlsContainer.appendChild(this.filePicker(instance));
183 controlsContainer.appendChild(this.extractKeyValuePairsButton(instance));
184 controlsContainer.appendChild(this.colorLegend());
185
186 // Add the controls container to the viewer element.
187 const element = document.getElementById('viewer');
188 element.insertBefore(this.jsonElement(), element.firstChild);
189 element.insertBefore(controlsContainer, element.firstChild);
190 };
191}
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('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_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 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, 'title_block_template.json');
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('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